11. diel - Aréna s mágom (dedičnosť a polymorfizmus)

V minulej lekcii, Dedičnosť a polymorfizmus v Pythone, sme si vysvetlili dedičnosť a polymorfizmus.

V dnešnom tutoriále objektovo orientovaného programovania v Pythone si vyskúšame dedičnosť a polymorfizmus v praxi. Bude to opäť na našej hre s bojovníkmi, kde z bojovníka zdedíme mága. Tento tutoriál už patrí k tým náročnejším a bude to tak aj pri ďalších. Preto si priebežne precvičujte prácu s objektmi, skúšajte si naše cvičenia a taktiež vymýšľajte svoje vlastné aplikácie, aby ste si zažili základné veci. To, že je tu prítomný celý seriál neznamená, že ho celý zrazu prečítate a pochopíte :) Snažte sa programovať priebežne.

Mág - Objektovo orientované programovanie v Pythone

Než začneme niečo písať, zhodnime sa na tom, čo by mal mág vedieť. Mág bude fungovať rovnako ako bojovník. Okrem života bude mať však aj manu. Spočiatku bude mana plná. V prípade plnej many môže mág vykonať magický útok, ktorý bude mať pravdepodobne vyššie poškodenie, než útok normálny (ale samozrejme záleží na tom, ako si ho nastavíme). Tento útok manu vybije na 0. Každé kolo sa bude mana zvyšovať o 10 a mág bude podnikať len bežný útok. Akonáhle sa mana úplne doplní, opäť bude môcť magický útok použiť. Mana bude zobrazená grafickým ukazovateľom, rovnako ako život.

Do pôvodného projektu ArenaFight vytvoríme teda triedu Mage, ktorú zdedíme z triedy Warrior. Opäť si pre ňu založíme samostatný súbor a nazveme ho mage.py Obsah súboru bude zatiaľ vyzerať takto (triedu si opäť okomentujte):

from warrior import Warrior

class Mage(Warrior):

Konštruktor potomka

Pôvodný konštruktor bojovníka použiť nemôžeme, pretože chceme mať u mága dva parametre navyše (manu a magický útok).

Definujeme si teda konštruktor v potomkovi, ktorý berie parametre potrebné na vytvorenie bojovníka a niekoľko parametrov navyše pre mága.

U potomkov nie je nutné vždy volať konštruktor predka. Náš konštruktor musí mať samozrejme všetky parametre potrebné pre predka plus tie nové, čo má navyše potomok, takže ho volať budeme. Niektoré potom odovzdáme predkovi a niektoré si spracujeme sami. Konštruktor predka sa vykoná pred naším konštruktorom.

V Pythone existuje metóda super(), ktorá zavolá verziu metódy na predkovi. My teda môžeme zavolať konštruktor predka s danými parametrami a potom vykonať naviac inicializáciu pre mága. V Pythone volaní konštruktora predka (metódy super()) píšeme do tela nášho konštruktora.

Konštruktor mága bude teda vyzerať takto:

def __init__(self, name, health, damage, defense, die, mana, magic_damage):
    super().__init__(name, health, damage, defense, die)
    self._mana = mana
    self._max_mana = mana
    self._magic_damage = magic_damage

Presuňme sa do hlavného programu v main.py a druhého bojovníka (Shadow) zmeňme na mága:

gandalf = Mage("Gandalf", 60, 15, 12, die, 30, 45)

Samozrejme musíme doplniť import triedy Mage a zámenu urobiť aj v riadku, kde bojovníka do arény vkladáme.

Polymorfizmus a prepisovanie metód

Bolo by výhodné, keby inštancia triedy Arena mohla s mágom pracovať rovnakým spôsobom ako s bojovníkom. My už vieme, že takémuto mechanizmu hovoríme polymorfizmus. Aréna zavolá na bojovníkovi metódu attack() so súperom v parametri. Nestará sa o to, či bude útok vykonávať bojovník alebo mág, bude s nimi pracovať rovnako. U mága si teda prepíšeme metódu attack() z predka. Zdedenú metódu prepíšeme tak, aby útok pracoval s manou. Hlavička metódy ale zostane rovnaká.

V potomkovi môžeme jednoducho a bez okolkov prepísať ľubovoľnú metódu. V Pythone sú všetky metódy - povedané terminológiou jazykov C++, C# - virtuálne.

Teraz sa vráťme k metóde attack(). V súbore s mágom (mage.py) ju prepíšeme (prekryjeme). Jej správanie nebude nijako zložité. Podľa hodnoty many buď vykonáme bežný útok alebo útok magický. Hodnotu many potom buď zvýšime o 10 alebo naopak znížime na 0 v prípade magického útoku:

def attack(self, enemy):
    # Mana isn't full
    if self._mana < self._max_mana:
        self._mana = self._mana + 10
        if self._mana > self._max_mana:
            self._mana = self._max_mana
        hit = self._damage + self._die.roll()
        message = f"{self._name} attacks with a hit worth {hit} hp."
        self._set_message(message)
    # Magic damage
    else:
        hit = self._magic_damage + self._die.roll()
        message = f"{self._name} used magic for {hit} hp."
        self._set_message(message)
        self._mana = 0
    enemy.defend(hit)

Kód je zrozumiteľný. Všimnime si ale obmedzenie many na max_mana. Môže sa nám totiž stať, že túto hodnotu presiahneme. Teraz sa na chvíľu zastavme a zamyslime sa nad kódom. Nemagický útok vyššie v podstate vykonáva pôvodná metóda attack(). Iste by teda bolo prínosné zavolať podobu metódy na predkovi namiesto toho, aby sme správanie opisovali. Na to opäť použijeme metódu super(), ktorá zavolá metódu attack() tak, ako je definovaná v rodičovskej triede Warrior:

Klikni pre editáciu
  • App
    • mage.py
    • warrior.py
    • main.py
    • rolling_die.py
    • arena.py
  •     def attack(self, enemy):
        # Mana isn't full
            if self._mana < self._max_mana:
                self._mana = self._mana + 10
                if self._mana > self._max_mana:
                    self._mana = self._max_mana
                super().attack(enemy)
        # Magic damage
            else:
                hit = self._magic_damage + self._die.roll()
                message = f"{self._name} used magic for {hit} hp."
                self._set_message(message)
                self._mana = 0
                enemy.defend(hit)
    
  • from rolling_die import RollingDie
    from warrior import Warrior
    from arena import Arena
    from mage import Mage
    # creating objects
    die = RollingDie(10)
    zalgoren = Warrior("Zalgoren", 100, 20, 10, die)
    gandalf = Mage("Gandalf", 60, 15, 12, die, 30, 45)
    arena = Arena(zalgoren, gandalf, die)
    # fight
    arena.fight()
    
  • class RollingDie:
    
        def __init__(self, sides_count=6):
            self._sides_count = sides_count
    
        def __str__(self):
            return str(f"Rolling die with {self._sides_count} sides.")
    
        def get_sides_count(self):
            return self._sides_count
    
        def roll(self):
            import random as _random
            return _random.randint(1, self._sides_count)
    
        def __repr__(self):
            return f"RollingDie({self._sides_count})"
    
  • from mage import Mage
    class Arena:
    
        def __init__(self, warrior_1, warrior_2, die):
            self._warrior_1 = warrior_1
            self._warrior_2 = warrior_2
            self._die = die
    
        def _render(self):
           # commented out due to the compiler
           # self._clear_screen()
            print("-------------- Arena -------------- \n")
            print("Warriors health: \n")
            print(f"{self._warrior_1} {self._warrior_1.health_bar()}")
            print(f"{self._warrior_2} {self._warrior_2.health_bar()}")
    
        """
        def _clear_screen(self):
            import os as _os
            _os.system('cls' if _os.name == 'nt' else 'clear')
        """
    
        def _print_message(self, message):
            import time as _time
            print(message)
            # commented out due to the compiler
            _time.sleep(0.75)
    
        def fight(self):
            import random as _random
            print("Welcome to the Arena!")
            print(f"Today {self._warrior_1} will battle against {self._warrior_2}!")
            print("Let the battle begin...", end=" ")
            input()
    
            # swapping the warriors
            if _random.randint(0, 1):
                (self._warrior_1, self._warrior_2) = (self._warrior_2,
                                                        self._warrior_1)
            # fight loop
            while self._warrior_1.is_alive() and self._warrior_2.is_alive():
                self._warrior_1.attack(self._warrior_2)
                self._render()
                self._print_message(self._warrior_1.get_last_message())
                self._print_message(self._warrior_2.get_last_message())
                if self._warrior_2.is_alive():
                    self._warrior_2.attack(self._warrior_1)
                    self._render()
                    self._print_message(self._warrior_2.get_last_message())
                    self._print_message(self._warrior_1.get_last_message())
    
    • Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.

    Opäť vidíme, ako môžeme znovupoužívať kód. S dedičnosťou je spojených naozaj mnoho techník, ako si ušetriť prácu. V našom prípade to ušetrí niekoľko riadkov, ale pri väčšom projekte by to mohlo mať obrovský význam. Ostatné nesúkromné metódy sa automaticky zdedia.

    Aplikácia teraz funguje tak, ako má:

    The game works correctly:
    -------------- Arena --------------
    
    Warriors health:
    
    Gandalf [####################]                  ]
    Zalgoren [#############       ]
    Gandalf used magic for 51 hp.
    Zalgoren defended against the attack but still lost 33 hp.

    Aréna nás však neinformuje o mane mága. Poďme to napraviť. Pridáme mágovi verejnú metódu mana_bar(), ktorá bude obdobne ako pri živote vracať reťazec s grafickým ukazovateľom many.

    Aby sme nemuseli logiku so zložením ukazovateľa písať dvakrát, upravíme metódu health_bar() v triede Warrior. Pripomeňme si, ako vyzerá:

    def health_bar(self):
        total = 20
        count  = int(self._health / self._max_health * total)
        if (count == 0 and self.is_alive()):
            count = 1
        return f"[{'#' * count}{' ' * (total - count)}]"

    Vidíme, že nie je okrem premenných health a max_health na živote nijako závislá. Metódu premenujeme na graphical_bar() a dáme jej dva parametre: aktuálnu hodnotu a maximálnu hodnotu. Premenné health a max_health v tele metódy potom nahradíme za current a maximum:

    def graphical_bar(self, current, maximum):
        total = 20
        count = int(current / maximum * total)
        if (count == 0 and self.is_alive()):
            count = 1
        return f"[{'#' * count}{' ' * (total - count)}]"

    Metódu health_bar() v triede Warrior naimplementujeme znova, bude nám v nej stačiť jediný riadok a to zavolanie metódy graphical_bar() s príslušnými parametrami:

    def health_bar(self):
        return self.graphical_bar(self._health, self._max_health)

    Určite sme mohli v tutoriáli s bojovníkom urobiť metódu graphical_bar() rovno. Chceli sme však ukázať, ako sa riešia prípady, keď potrebujeme vykonať podobnú funkčnosť viackrát. S takouto parametrizáciou sa v prax stretneme často, pretože nikdy presne nevieme, čo budeme v budúcnosti od nášho programu požadovať.

    Teraz môžeme vykresľovať ukazovateľ tak, ako sa nám to hodí. Presuňme sa do triedy Mage a naimplementujme metódu mana_bar():

    def mana_bar(self):
        return self.graphical_bar(self._mana, self._max_mana)

    Jednoduché, že? Teraz je mág hotový, zostáva len naučiť arénu zobrazovať manu v prípade, že je bojovník mág. Presuňme sa teda do triedy Arena.

    Rozpoznanie typu objektu

    Keďže sa nám teraz vykreslenie bojovníka skomplikovalo, urobíme si naň v aréne samostatnú metódu print_warrior(). Jej parametrom bude daná inštancia bojovníka:

    def _print_warrior(self, warrior):
        print(warrior)
        print(f"Health: {warrior.health_bar()}")

    Teraz poďme reagovať na to, či je bojovník mág. Už sme si povedali, že na to slúži funkcia isinstance():

    def _print_warrior(self, warrior):
        print(warrior)
        print(f"Health: {warrior.health_bar()}")
        if isinstance(warrior, Mage):
            print(f"Mana: {warrior.mana_bar()}")

    To by sme mali, print_warrior() budeme volať v metóde render(), ktorá bude vyzerať takto:

    Klikni pre editáciu
    • App
      • arena.py
      • main.py
      • rolling_die.py
      • warrior.py
      • mage.py
    •     def _render(self):
             # commented out due to the compiler
             # self._clear_screen()
              print("-------------- Arena -------------- \n")
              print("Warriors: \n")
              self._print_warrior(self._warrior_1)
              self._print_warrior(self._warrior_2)
      
      • Skontroluj, či výstupy programu zodpovedajú predlohe. S inými textami testy neprejdú.

      A máme hotovo :)

      Aplikáciu ešte môžeme dodať krajší vzhľad. Vložíme napríklad ASCIIart nadpis Arena, ktorý vytvoríme ASCII generátorom. Metódu na vykreslenie ukazovateľa upravíme tak, aby vykresľovala plný obdĺžnik miesto # (ten napíšeme pomocou klávesov Alt + 2 1 9). Výsledok bude vyzerať takto:

      Modified version of the application:
         __    ____  ____  _  _    __
        /__\  (  _ \( ___)( \( )  /__\
       /(__)\  )   / )__)  )  (  /(__)\
      (__)(__)(_)\_)(____)(_)\_)(__)(__)
      
      Warriors:
      
      Zalgoren
      Health: ████
      
      Gandalf
      Health: ███████
      Mana:  █
      
      Gandalf used magic for 48 hp
      Zalgoren defended against the attack but still lost 33 hp

      Kód je k dispozícii v prílohe. Pokiaľ ste niečomu nerozumeli, skúste si článok prečítať viackrát alebo pomalšie, sú to dôležité praktiky.

      V nasledujúcom kvíze, Kvíz - Dedičnosť a polymorfizmus v Pythone, si vyskúšame nadobudnuté skúsenosti z predchádzajúcich lekcií.


       

      Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

      Stiahnuť

      Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

      Stiahnuté 0x (12.65 kB)
      Aplikácia je vrátane zdrojových kódov v jazyku Python

       

      Ako sa ti páči článok?
      Pred uložením hodnotenia, popíš prosím autorovi, čo je zleZnakov 0 z 50-500
      Predchádzajúci článok
      Dedičnosť a polymorfizmus v Pythone
      Všetky články v sekcii
      Objektovo orientované programovanie v Pythone
      Preskočiť článok
      (neodporúčame)
      Kvíz - Dedičnosť a polymorfizmus v Pythone
      Článok pre vás napísal gcx11
      Avatar
      Užívateľské hodnotenie:
      3 hlasov
      (^_^)
      Aktivity