Creature Battle Arena: operatoren en exceptions

Deze pagina bevat uitvoerbare code.

Opdracht: Creature Battle Arena: operatoren en exceptions

Zo ziet creatures.py eruit aan het eind van het practicum: eerst de klassen en de functies, daarna de assertions van week 5 en 6 en per stap die van week 7, die onderaan in het bestand staan.

import random


class Creature:
    """Een wezen met hp, een aanvalskracht en een verdediging."""

    def __init__(self, name, hp, attack, defense):
        """Maak een wezen op level 1; gooit een ValueError bij foute invoer."""
        if hp <= 0:
            raise ValueError("hp moet positief zijn")
        if attack < 1:
            raise ValueError("attack moet minstens 1 zijn")
        if defense < 0:
            raise ValueError("defense mag niet negatief zijn")
        self.name = name
        self._level = 1
        self._max_hp = hp
        self._hp = hp
        self._attack_power = attack
        self.defense = defense

    def __repr__(self):
        """Geeft het wezen als string, met alles wat je bij het debuggen wilt weten."""
        return (
            f"{self.__class__.__name__}({self.name}, level {self._level}, "
            f"hp {self._hp}/{self._max_hp}, attack {self._attack_power}, "
            f"defense {self.defense})"
        )

    def take_damage(self, amount):
        """Verlaagt hp met amount min de verdediging en geeft die schade terug."""
        actual = max(0, amount - self.defense)
        self._hp = max(0, self._hp - actual)
        return actual

    def heal(self, amount):
        """Verhoogt hp met amount, nooit boven het maximum."""
        self._hp = min(self._max_hp, self._hp + amount)

    def is_alive(self):
        """Geeft True zolang hp groter is dan 0."""
        return self._hp > 0

    def is_critical(self):
        """Geeft True als hp een kwart van het maximum is, of minder."""
        return self._hp <= self._max_hp * 0.25

    @property
    def hp(self):
        """De hp; een nieuwe waarde blijft tussen 0 en het maximum."""
        return self._hp

    @hp.setter
    def hp(self, value):
        """Geeft hp de waarde value, maar nooit onder 0 of boven het maximum."""
        self._hp = max(0, min(value, self._max_hp))

    @property
    def level(self):
        """Het level, alleen om te lezen."""
        return self._level

    def attack(self, target, bonus=0):
        """Laat target schade nemen ter grootte van de aanvalskracht plus bonus."""
        return target.take_damage(self._attack_power + bonus)

    def is_stronger_than(self, other):
        """Geeft True als de eigen aanvalskracht groter is dan die van other."""
        return self._attack_power > other._attack_power

    def special_move(self, target):
        """Gooit een NotImplementedError: elke subklasse schrijft een eigen versie."""
        name = self.__class__.__name__
        raise NotImplementedError(f"{name} moet special_move overschrijven")

    def level_up(self):
        """Laat het wezen één level groeien."""
        self._level += 1
        old_max = self._max_hp
        self._max_hp = int(self._max_hp * 1.15)
        self._hp = int(self._hp * (self._max_hp / old_max))
        self._attack_power += 3
        self.defense += 2

    def power_score(self):
        """Geeft de kracht: hp, aanvalskracht en verdediging bij elkaar opgeteld."""
        return self._hp + self._attack_power + self.defense

    def __eq__(self, other):
        """Geeft True als other een wezen is dat even sterk is."""
        if not isinstance(other, Creature):
            return False
        return self.power_score() == other.power_score()

    def __lt__(self, other):
        """Geeft True als het wezen zwakker is dan other."""
        if not isinstance(other, Creature):
            raise TypeError("een wezen is alleen met een wezen te vergelijken")
        return self.power_score() < other.power_score()

    def __le__(self, other):
        """Geeft True als het wezen zwakker is dan other, of even sterk."""
        if not isinstance(other, Creature):
            raise TypeError("een wezen is alleen met een wezen te vergelijken")
        return self.power_score() <= other.power_score()

    def __mul__(self, n):
        """Geeft een party met n nieuwe wezens, elk zoals dit wezen begon."""
        if not isinstance(n, int):
            raise TypeError("een wezen kun je alleen met een integer vermenigvuldigen")
        if n < 0:
            raise ValueError(
                "een wezen kun je niet met een negatief getal vermenigvuldigen"
            )
        copies = [
            self.__class__(self.name, self._max_hp, self._attack_power, self.defense)
            for _ in range(n)
        ]
        return Party(copies)
class Party:
    """Een groep wezens die zich via hun methoden laat aansturen."""

    def __init__(self, creatures):
        """Maak een party met een kopie van de lijst creatures."""
        self._members = list(creatures)

    def add(self, creature):
        """Voegt creature toe aan de party."""
        self._members.append(creature)

    def alive_members(self):
        """Geeft een lijst van de leden die nog leven."""
        return [creature for creature in self._members if creature.is_alive()]

    def critical_members(self):
        """Geeft een lijst van de leden die er kritiek aan toe zijn."""
        return [creature for creature in self._members if creature.is_critical()]

    def strongest_attacker(self):
        """Geeft het lid dat het hardst aanvalt, of None als de party leeg is."""
        strongest = None
        for creature in self._members:
            if strongest is None or creature.is_stronger_than(strongest):
                strongest = creature
        return strongest

    def heal_all(self, amount):
        """Laat elk levend lid genezen met amount."""
        for creature in self.alive_members():
            creature.heal(amount)

    def __add__(self, other):
        """Geeft een nieuwe party met de leden van deze party en van other."""
        if not isinstance(other, Party):
            raise TypeError("bij een party kun je alleen een party optellen")
        return Party(self._members + other._members)

    def __sub__(self, creature):
        """Geeft een nieuwe party zonder creature."""
        if not isinstance(creature, Creature):
            raise TypeError("uit een party kun je alleen een wezen halen")
        return Party([member for member in self._members if member is not creature])
class Beast(Creature):
    """Een wezen dat woedend kan worden, en dan harder aanvalt."""

    def __init__(self, name, hp, attack, defense):
        """Maak een beest dat nog niet woedend is."""
        super().__init__(name, hp, attack, defense)
        self.enraged = False

    def enrage(self):
        """Maakt het beest woedend."""
        self.enraged = True

    def rage_bonus(self, amount):
        """Geeft amount als het beest woedend is, en anders 0."""
        if self.enraged:
            return amount
        return 0


class Dragon(Beast):
    """Een draak, die vuur spuwt."""

    def __init__(self, name, hp=120, attack=25, defense=10):
        """Maak een draak; zonder verdere argumenten met de standaardwaarden."""
        super().__init__(name, hp, attack, defense)

    def special_move(self, target):
        """Spuwt vuur naar target, harder als de draak woedend is."""
        damage = self.attack(target, self.rage_bonus(10))
        return f"{self.name} spuwt vuur en doet {damage} schade!"


class Wolf(Beast):
    """Een wolf, die woedend wordt van zijn eigen aanval."""

    def __init__(self, name, hp=50, attack=15, defense=5):
        """Maak een wolf; zonder verdere argumenten met de standaardwaarden."""
        super().__init__(name, hp, attack, defense)

    def special_move(self, target):
        """Bespringt target, en wordt daarna woedend."""
        damage = self.attack(target, self.rage_bonus(5))
        self.enrage()
        return (
            f"{self.name} huilt en bespringt zijn prooi voor {damage} schade, "
            "en wordt woedend!"
        )


class Goblin(Creature):
    """Een goblin, die met stenen gooit."""

    def __init__(self, name, hp=40, attack=8, defense=2):
        """Maak een goblin; zonder verdere argumenten met de standaardwaarden."""
        super().__init__(name, hp, attack, defense)

    def special_move(self, target):
        """Gooit een steen naar target."""
        damage = self.attack(target)
        return f"{self.name} gooit een puntige steen en doet {damage} schade!"


class Healer(Creature):
    """Een genezer, die zichzelf geneest in plaats van aan te vallen."""

    def __init__(self, name, hp=60, attack=3, defense=5):
        """Maak een genezer; zonder verdere argumenten met de standaardwaarden."""
        super().__init__(name, hp, attack, defense)

    def special_move(self, target):
        """Geneest zichzelf met 15, en doet niets met target."""
        self.heal(15)
        return f"{self.name} spreekt een genezingsspreuk uit en herstelt 15 hp."
class Turret:
    """Een wachttoren: geen wezen, maar hij vecht wel mee."""

    def __init__(self):
        """Maak een wachttoren die nog geen klap heeft gehad."""
        self.name = "Wachttoren"
        self._hits_taken = 0
        self._max_hits = 3
        self._min_damage = 5
        self._max_damage = 25

    def __repr__(self):
        """Geeft de wachttoren als string, met het aantal klappen."""
        return f"Turret({self.name}, hits {self._hits_taken}/{self._max_hits})"

    def take_damage(self, amount):
        """Telt één klap, hoe groot amount ook is, en geeft amount terug."""
        self._hits_taken += 1
        return amount

    def is_alive(self):
        """Geeft True zolang de toren minder klappen heeft gehad dan hij aankan."""
        return self._hits_taken < self._max_hits

    def attack(self, target):
        """Laat target een willekeurige schade nemen en geeft die terug."""
        return target.take_damage(random.randint(self._min_damage, self._max_damage))

    def special_move(self, target):
        """Vuurt op target en beschrijft wat er gebeurde."""
        damage = self.attack(target)
        return f"{self.name} vergrendelt en vuurt, en doet {damage} schade!"
def battle_round(attacker, defender):
    """Laat attacker zijn speciale zet doen op defender, en meldt de afloop."""
    print(attacker.special_move(defender))
    if defender.is_alive():
        status = "levend"
    else:
        status = "verslagen"
    print(f"{defender.name} is nu {status}.")


def attack_all(attackers, defenders):
    """Laat elk levend lid van attackers het eerste levende lid van defenders aanvallen."""
    for attacker in attackers.alive_members():
        targets = defenders.alive_members()
        if len(targets) == 0:
            return
        battle_round(attacker, targets[0])


def battle(side_a, side_b, max_rounds):
    """Laat side_a en side_b vechten tot een partij verslagen is; geeft het aantal ronden."""
    rounds = 0
    while (
        rounds < max_rounds
        and len(side_a.alive_members()) > 0
        and len(side_b.alive_members()) > 0
    ):
        rounds += 1
        attack_all(side_a, side_b)
        attack_all(side_b, side_a)
    return rounds

Waar je begint

De assertions van week 5 en 6, zoals ze na stap 6 en 7 overblijven: zonder die over Creature("Pech", -5, 0, -3) en over de speciale zet van een gewoon Creature en van een Beast, en met pop.hp == 175 in plaats van pop.hp == 165.

assert repr(Creature("Vlam", 120, 25, 10)) == (
    "Creature(Vlam, level 1, hp 120/120, attack 25, defense 10)"
)

grom = Creature("Grom", 40, 8, 2)
assert grom.take_damage(12) == 10
assert grom.is_alive()
assert not grom.is_critical()
assert grom.take_damage(22) == 20
assert grom.is_critical()
grom.heal(100)
assert not grom.is_critical()
assert grom.take_damage(1) == 0
assert grom.take_damage(500) == 498
assert not grom.is_alive()

vlam = Creature("Vlam", 120, 25, 10)
assert vlam.hp == 120
vlam.hp = 200
assert vlam.hp == 120
vlam.hp = -5
assert vlam.hp == 0
assert vlam.level == 1

vlam = Creature("Vlam", 120, 25, 10)
grom = Creature("Grom", 40, 8, 2)
assert vlam.attack(grom) == 23
assert grom.hp == 17
assert grom.attack(vlam) == 0
assert vlam.is_stronger_than(grom)
assert not grom.is_stronger_than(vlam)
grom.level_up()
assert grom.level == 2
assert repr(grom) == "Creature(Grom, level 2, hp 19/46, attack 11, defense 4)"

vlam = Creature("Vlam", 120, 25, 10)
grom = Creature("Grom", 40, 8, 2)
mos = Creature("Mos", 60, 3, 5)
party = Party([grom, mos])
party.add(vlam)
assert party.strongest_attacker() is vlam
assert len(party.alive_members()) == 3

vlam.attack(grom)
vlam.attack(grom)
assert not grom.is_alive()
assert len(party.alive_members()) == 2
assert len(party.critical_members()) == 1

vlam.attack(mos)
party.heal_all(10)
assert mos.hp == 50
assert grom.hp == 0

assert Party([]).strongest_attacker() is None

members = [mos]
small = Party(members)
members.append(vlam)
assert len(small.alive_members()) == 1
vlam = Dragon("Vlam")
grom = Goblin("Grom")
mos = Healer("Mos")
assert repr(vlam) == "Dragon(Vlam, level 1, hp 120/120, attack 25, defense 10)"
assert repr(grom) == "Goblin(Grom, level 1, hp 40/40, attack 8, defense 2)"
assert repr(Healer("Varen", 80)) == (
    "Healer(Varen, level 1, hp 80/80, attack 3, defense 5)"
)
assert vlam.special_move(grom) == "Vlam spuwt vuur en doet 23 schade!"
assert grom.special_move(mos) == "Grom gooit een puntige steen en doet 3 schade!"
assert grom.special_move(vlam) == "Grom gooit een puntige steen en doet 0 schade!"
mos.take_damage(30)
assert mos.hp == 32
assert mos.special_move(vlam) == (
    "Mos spreekt een genezingsspreuk uit en herstelt 15 hp."
)
assert mos.hp == 47
assert vlam.is_stronger_than(grom)

pop = Creature("Pop", 200, 1, 0)
for creature in [Dragon("Vlam"), Goblin("Grom"), Healer("Mos")]:
    before = pop.hp
    print(creature.special_move(pop))
    print(f"hp van de pop: {before} -> {pop.hp}")

party = Party([Goblin("Grom"), Dragon("Vlam"), Healer("Mos")])
assert party.strongest_attacker().name == "Vlam"
assert len(party.alive_members()) == 3
assert pop.hp == 167

brul = Beast("Brul", 50, 10, 0)
pop = Creature("Pop", 200, 1, 0)
assert brul.rage_bonus(5) == 0
assert brul.attack(pop, brul.rage_bonus(5)) == 10
brul.enrage()
assert brul.rage_bonus(5) == 5
assert brul.attack(pop, brul.rage_bonus(5)) == 15
assert pop.hp == 175

pop = Creature("Pop", 200, 1, 0)
grijs = Wolf("Grijs")
assert grijs.special_move(pop) == (
    "Grijs huilt en bespringt zijn prooi voor 15 schade, en wordt woedend!"
)
assert grijs.special_move(pop) == (
    "Grijs huilt en bespringt zijn prooi voor 20 schade, en wordt woedend!"
)
vlam = Dragon("Vlam")
assert vlam.special_move(pop) == "Vlam spuwt vuur en doet 25 schade!"
assert vlam.special_move(pop) == "Vlam spuwt vuur en doet 25 schade!"
vlam.enrage()
assert vlam.special_move(pop) == "Vlam spuwt vuur en doet 35 schade!"
assert pop.hp == 80
assert repr(Wolf("Grijs")) == "Wolf(Grijs, level 1, hp 50/50, attack 15, defense 5)"

stro = Creature("Stro", 1000, 1, 0)
toren = Turret()
for _ in range(20):
    assert 5 <= toren.attack(stro) <= 25

toren = Turret()
toren.take_damage(1000)
assert toren.is_alive()
toren.take_damage(1)
assert toren.is_alive()
toren.take_damage(1)
assert not toren.is_alive()
assert repr(toren) == "Turret(Wachttoren, hits 3/3)"

toren = Turret()
vlam = Dragon("Vlam")
assert vlam.attack(toren) == 25
assert toren.special_move(vlam).startswith("Wachttoren vergrendelt en vuurt")
assert isinstance(vlam, Creature)
assert not isinstance(toren, Creature)

vlam = Dragon("Vlam")
grom = Goblin("Grom")
battle_round(vlam, grom)
battle_round(vlam, grom)
assert not grom.is_alive()

toren = Turret()
battle_round(vlam, toren)
assert toren.is_alive()
battle_round(toren, vlam)
assert 105 <= vlam.hp <= 120

vlam = Dragon("Vlam")
grom = Goblin("Grom")
assert battle(Party([vlam]), Party([grom]), 10) == 2
assert not grom.is_alive()
assert vlam.hp == 120

assert battle(Party([Healer("Mos")]), Party([Healer("Varen")]), 5) == 5

team = Party([Wolf("Grijs"), Turret()])
enemies = Party([Goblin("Grom"), Goblin("Klauw")])
battle(team, enemies, 20)
assert len(enemies.alive_members()) == 0
assert len(team.alive_members()) == 2
Vlam spuwt vuur en doet 25 schade!
hp van de pop: 200 -> 175
Grom gooit een puntige steen en doet 8 schade!
hp van de pop: 175 -> 167
Mos spreekt een genezingsspreuk uit en herstelt 15 hp.
hp van de pop: 167 -> 167
Vlam spuwt vuur en doet 23 schade!
Grom is nu levend.
Vlam spuwt vuur en doet 23 schade!
Grom is nu verslagen.
Vlam spuwt vuur en doet 25 schade!
Wachttoren is nu levend.
Wachttoren vergrendelt en vuurt, en doet 1 schade!
Vlam is nu levend.
Vlam spuwt vuur en doet 23 schade!
Grom is nu levend.
Grom gooit een puntige steen en doet 0 schade!
Vlam is nu levend.
Vlam spuwt vuur en doet 23 schade!
Grom is nu verslagen.
Mos spreekt een genezingsspreuk uit en herstelt 15 hp.
Varen is nu levend.
Varen spreekt een genezingsspreuk uit en herstelt 15 hp.
Mos is nu levend.
Mos spreekt een genezingsspreuk uit en herstelt 15 hp.
Varen is nu levend.
Varen spreekt een genezingsspreuk uit en herstelt 15 hp.
Mos is nu levend.
Mos spreekt een genezingsspreuk uit en herstelt 15 hp.
Varen is nu levend.
Varen spreekt een genezingsspreuk uit en herstelt 15 hp.
Mos is nu levend.
Mos spreekt een genezingsspreuk uit en herstelt 15 hp.
Varen is nu levend.
Varen spreekt een genezingsspreuk uit en herstelt 15 hp.
Mos is nu levend.
Mos spreekt een genezingsspreuk uit en herstelt 15 hp.
Varen is nu levend.
Varen spreekt een genezingsspreuk uit en herstelt 15 hp.
Mos is nu levend.
Grijs huilt en bespringt zijn prooi voor 13 schade, en wordt woedend!
Grom is nu levend.
Wachttoren vergrendelt en vuurt, en doet 23 schade!
Grom is nu levend.
Grom gooit een puntige steen en doet 3 schade!
Grijs is nu levend.
Klauw gooit een puntige steen en doet 3 schade!
Grijs is nu levend.
Grijs huilt en bespringt zijn prooi voor 18 schade, en wordt woedend!
Grom is nu verslagen.
Wachttoren vergrendelt en vuurt, en doet 18 schade!
Klauw is nu levend.
Klauw gooit een puntige steen en doet 3 schade!
Grijs is nu levend.
Grijs huilt en bespringt zijn prooi voor 18 schade, en wordt woedend!
Klauw is nu levend.
Wachttoren vergrendelt en vuurt, en doet 11 schade!
Klauw is nu verslagen.

Stap 1: even sterk

vlam = Dragon("Vlam")
ember = Dragon("Ember")
grom = Goblin("Grom")
assert vlam.power_score() == 155
assert grom.power_score() == 50
assert vlam == ember
assert vlam is not ember
assert vlam != grom
assert Creature("Klont", 50, 15, 5) == Wolf("Grijs")
assert not vlam == "Vlam"
grom.take_damage(12)
assert grom.power_score() == 40

Stap 2: sorteren

party = Party([Dragon("Vlam"), Goblin("Grom"), Wolf("Grijs"), Healer("Mos")])
names = [creature.name for creature in sorted(party.alive_members())]
assert names == ["Grom", "Mos", "Grijs", "Vlam"]
assert Goblin("Grom") < Healer("Mos")
assert Goblin("Grom") <= Goblin("Klauw")
assert not Goblin("Grom") < Goblin("Klauw")

Stap 3: groter dan, zonder het te schrijven

vlam = Dragon("Vlam")
grom = Goblin("Grom")
assert vlam > grom
assert vlam >= grom
assert not grom > vlam
assert vlam >= Dragon("Ember")

Stap 4: partijen optellen en aftrekken

grom = Goblin("Grom")
klauw = Goblin("Klauw")
vlam = Dragon("Vlam")
goblins = Party([grom, klauw])
both = goblins + Party([vlam])
assert [creature.name for creature in both.alive_members()] == ["Grom", "Klauw", "Vlam"]
assert len(goblins.alive_members()) == 2
assert grom == klauw
rest = both - klauw
assert [creature.name for creature in rest.alive_members()] == ["Grom", "Vlam"]
assert len(both.alive_members()) == 3

Stap 5: een wezen vermenigvuldigen

vlam = Dragon("Vlam")
vlam.take_damage(40)
army = vlam * 3
members = army.alive_members()
assert len(members) == 3
assert repr(members[0]) == "Dragon(Vlam, level 1, hp 120/120, attack 25, defense 10)"
assert members[0] is not members[1]
assert members[0] is not vlam
assert vlam.hp == 90
pop = Creature("Pop", 200, 1, 0)
assert members[2].special_move(pop) == "Vlam spuwt vuur en doet 25 schade!"
assert len((vlam * 0).alive_members()) == 0
vlam = Dragon("Vlam")
others = Party([Goblin("Grom"), Healer("Mos")])
big = vlam * 3 + others
assert len(big.alive_members()) == 5
bench = big - big.strongest_attacker()
assert len(bench.alive_members()) == 4
assert len(others.alive_members()) == 2
assert vlam.hp == 120

vlam * -1 en vlam * 2.5 geven de foutmeldingen uit __mul__. Die teksten kies je zelf, dus de jouwe mogen anders zijn.

vlam * -1
ValueError: een wezen kun je niet met een negatief getal vermenigvuldigen
vlam * 2.5
TypeError: een wezen kun je alleen met een integer vermenigvuldigen

Stap 6: de constructor weigert foute invoer

assert repr(Creature("Vlam", 120, 25, 10)) == (
    "Creature(Vlam, level 1, hp 120/120, attack 25, defense 10)"
)
assert repr(Creature("Stro", 1, 1, 0)) == (
    "Creature(Stro, level 1, hp 1/1, attack 1, defense 0)"
)
assert repr(Dragon("Vlam")) == (
    "Dragon(Vlam, level 1, hp 120/120, attack 25, defense 10)"
)

Foute invoer geeft de foutmelding uit het practicum.

Creature("Pech", -5, 0, -3)
ValueError: hp moet positief zijn

Stap 7: special_move afdwingen

pop = Creature("Pop", 200, 1, 0)
assert Dragon("Vlam").special_move(pop) == "Vlam spuwt vuur en doet 25 schade!"
assert Wolf("Grijs").special_move(pop).startswith("Grijs huilt")
assert battle(Party([Dragon("Vlam")]), Party([Goblin("Grom")]), 10) == 2
Vlam spuwt vuur en doet 23 schade!
Grom is nu levend.
Grom gooit een puntige steen en doet 0 schade!
Vlam is nu levend.
Vlam spuwt vuur en doet 23 schade!
Grom is nu verslagen.

Een Beast heeft geen eigen special_move, dus gooit de versie van Creature de exception.

brul = Beast("Brul", 50, 10, 0)
brul.special_move(Creature("Pop", 200, 1, 0))
NotImplementedError: Beast moet special_move overschrijven

Stap 8: de hp-setter

De setter is gebleven zoals hij was; zo slagen ook de assertions over hp uit week 5. Een setter die een ValueError gooit, is de andere keuze uit stap 8 van het practicum.

vlam = Dragon("Vlam")
vlam.take_damage(500)
assert vlam.hp == 0
vlam.hp = 60
assert vlam.hp == 60
vlam.heal(1000)
assert vlam.hp == 120

Wat duck typing niet belooft, opnieuw

sorted loopt vast op de wachttoren, met de foutmelding uit het practicum.

team = Party([Wolf("Grijs"), Turret()])
sorted(team.alive_members())
TypeError: '<' not supported between instances of 'Turret' and 'Wolf'