NOVINKA: Získej 40 hodin praktických dovedností s AI – ZDARMA ke každému akreditovanému kurzu!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 12 - Aréna s mágem (dědičnost a polymorfismus)

V minulé lekci, Dědičnost a polymorfismus v Pythonu, jsme si vysvětlili dědičnost a polymorfismus.

V dnešním tutoriálu objektově orientovaného programování v Pythonu si vyzkoušíme dědičnost a polymorfismus v praxi. Bude to opět na naší hře Tahový boj, kde z bojovníka podědíme mága. Tento tutoriál již patří k těm náročnějším a bude tomu tak i u dalších. Proto si průběžně procvičujte práci s objekty, zkoušejte si naše cvičení a také vymýšlejte své vlastní aplikace, abyste si zažili základní věci. To, že je tu přítomen celý seriál neznamená, že ho celý najednou přečtete a pochopíte :) Snažte se programovat průběžně.

Mág - Objektově orientované programování v Pythonu

Než začneme něco psát, shodněme se na tom, co by měl mág umět. Mág bude fungovat stejně jako bojovník. Kromě života bude mít však i manu. Zpočátku bude mana plná. V případě plné many může mág vykonat magický útok, který bude mít pravděpodobně vyšší poškození, než útok normální (ale samozřejmě záleží na tom, jak si ho nastavíme). Tento útok manu vybije na 0. Každé kolo se bude mana zvyšovat o 10 a mág bude podnikat jen běžný útok. Jakmile se mana zcela doplní, opět bude moci magický útok použít. Mana bude zobrazena grafickým ukazatelem, stejně jako život.

Do původního projektu TahovyBoj vytvoříme tedy třídu Mag, kterou zdědíme z třídy Bojovnik. Opět si pro ni založíme samostatný soubor a nazveme ho mag.py. Obsah souboru bude zatím vypadat takto (třídu si opět okomentujte):

from bojovnik import Bojovnik

class Mag(Bojovnik):

Konstruktor potomka

Původní konstruktor bojovníka použít nemůžeme, neboť chceme mít u mága dva parametry navíc (manu a magický útok).

Definujeme si tedy konstruktor v potomkovi, který bere parametry potřebné pro vytvoření bojovníka a několik parametrů navíc pro mága.

U potomků není nutné vždy volat konstruktor předka. Náš konstruktor musí mít samozřejmě všechny parametry potřebné pro předka plus ty nové, co má navíc potomek, takže ho volat budeme. Některé potom předáme předkovi a některé si zpracujeme sami. Konstruktor předka se vykoná před naším konstruktorem.

V Pythonu existuje metoda super(), která zavolá verzi metody na předkovi. My tedy můžeme zavolat konstruktor předka s danými parametry a poté vykonat navíc inicializaci pro mága. V Pythonu volání konstruktoru předka (metody super()) píšeme do těla našeho konstruktoru.

Konstruktor mága bude tedy vypadat takto:

def __init__(self, jmeno, zivot, utok, obrana, kostka, mana, magicky_utok):
    super().__init__(jmeno, zivot, utok, obrana, kostka)
    self._mana = mana
    self._max_mana = mana
    self._magicky_utok = magicky_utok

Přesuňme se do hlavního programu v main.py a druhého bojovníka (Shadow) změňme na mága:

gandalf = Mag("Gandalf", 60, 15, 12, kostka, 30, 45)

Samozřejmě musíme doplnit import třídy Mag a záměnu udělat i v řádku, kde bojovníka do arény vkládáme.

Polymorfismus a přepisování metod

Bylo by výhodné, kdyby instance třídy Arena mohla s mágem pracovat stejným způsobem jako s bojovníkem. My již víme, že takovémuto mechanismu říkáme polymorfismus. Aréna zavolá na bojovníkovi metodu utoc() se soupeřem v parametru. Nestará se o to, jestli bude útok vykonávat bojovník nebo mág, bude s nimi pracovat stejně. U mága si tedy přepíšeme metodu utoc() z předka. Zděděnou metodu přepíšeme tak, aby útok pracoval s manou. Hlavička metody ale zůstane stejná.

V potomkovi můžeme jednoduše a bez okolků přepsat libovolnou metodu. V Pythonu jsou všechny metody - řečeno terminologií jazyků C++, C# - virtuální.

Nyní se vraťme k metodě utoc(). V souboru s mágem (mag.py) ji přepíšeme (překryjeme). Její chování nebude nijak složité. Podle hodnoty many buď provedeme běžný útok nebo útok magický. Hodnotu many potom buď zvýšíme o 10 nebo naopak snížíme na 0 v případě magického útoku:

def utoc(self, souper):
    # mana není naplněna
    if self._mana < self._max_mana:
        self._mana = self._mana + 10
        if self._mana > self._max_mana:
            self._mana = self._max_mana
        uder = self._utok + self._kostka.hod()
        zprava = f"{self._jmeno} útočí s úderem za {uder} hp."
        self._nastav_zpravu(zprava)
    #magický útok
    else:
        uder = self._magicky_utok + self._kostka.hod()
        zprava = f"{self._jmeno} použil magii za {uder} hp."
        self._nastav_zpravu(zprava)
        self._mana = 0
        souper.bran_se(uder)

Kód je srozumitelný. Všimněme si ale omezení many na max_mana. Může se nám totiž stát, že tuto hodnotu přesáhneme. Nyní se na chvíli zastavme a zamysleme se nad kódem. Nemagický útok výše v podstatě vykonává původní metoda utoc(). Jistě by tedy bylo přínosné zavolat podobu metody na předkovi místo toho, abychom chování opisovali. K tomu opět použijeme metodu super(), která zavolá metodu utoc() tak, jak je definovaná v rodičovské třídě Bojovnik:

Klikni pro editaci
  • App
    • mag.py
    • bojovnik.py
    • main.py
    • kostka.py
    • arena.py
  •     def utoc(self, souper):
        # mana není naplněna
            if self._mana < self._max_mana:
                self._mana = self._mana + 10
                if self._mana > self._max_mana:
                    self._mana = self._max_mana
                super().utoc(souper)
        #magický útok
            else:
                uder = self._magicky_utok + self._kostka.hod()
                zprava = f"{self._jmeno} použil magii za {uder} hp."
                self._nastav_zpravu(zprava)
                self._mana = 0
                souper.bran_se(uder)
    
  • from kostka import Kostka
    from bojovnik import Bojovnik
    from arena import Arena
    from mag import Mag
    # vytvoření objektů
    kostka = Kostka(10)
    zalgoren = Bojovnik("Zalgoren", 100, 20, 10, kostka)
    gandalf = Mag("Gandalf", 60, 15, 12, kostka, 30, 45)
    arena = Arena(zalgoren, gandalf, kostka)
    # zápas
    arena.zapas()
    
  • class Kostka:
    
        def __init__(self, pocet_sten=6):
            self._pocet_sten = pocet_sten
    
        def __str__(self):
            return str(f"Kostka s {self._pocet_sten} stěnami.")
    
        def vrat_pocet_sten(self):
            return self._pocet_sten
    
        def hod(self):
            import random as _random
            return _random.randint(1, self._pocet_sten)
    
        def __repr__(self):
            return f"Kostka({self._pocet_sten})"
    
  • from mag import Mag
    class Arena:
    
        def __init__(self, bojovnik_1, bojovnik_2, kostka):
            self._bojovnik_1 = bojovnik_1
            self._bojovnik_2 = bojovnik_2
            self._kostka = kostka
    
        def _vykresli(self):
           # zakomentováno kvůli kompileru
           # self._vycisti_obrazovku()
            print("-------------- Aréna -------------- \n")
            print("Zdraví bojovníků: \n")
            print(f"{self._bojovnik_1} {self._bojovnik_1.graficky_zivot()}")
            print(f"{self._bojovnik_2} {self._bojovnik_2.graficky_zivot()}")
    
        """
        def _vycisti_obrazovku(self):
            import os as _os
            _os.system('cls' if _os.name == 'nt' else 'clear')
        """
    
        def _vypis_zpravu(self, zprava):
            import time as _time
            print(zprava)
            # zakomentováno kvůli kompileru
            _time.sleep(0.75)
    
        def zapas(self):
            import random as _random
            print("Vítejte v aréně!")
            print(f"Dnes se utkají {self._bojovnik_1} a {self._bojovnik_2}!")
            print("Zápas může začít...", end=" ")
            input()
    
            # prohození bojovníků
            if _random.randint(0, 1):
                (self._bojovnik_1, self._bojovnik_2) = (self._bojovnik_2,
                                                        self._bojovnik_1)
            # cyklus s bojem
            while self._bojovnik_1.je_nazivu() and self._bojovnik_2.je_nazivu():
                self._bojovnik_1.utoc(self._bojovnik_2)
                self._vykresli()
                self._vypis_zpravu(self._bojovnik_1.vrat_posledni_zpravu())
                self._vypis_zpravu(self._bojovnik_2.vrat_posledni_zpravu())
                if self._bojovnik_2.je_nazivu():
                    self._bojovnik_2.utoc(self._bojovnik_1)
                    self._vykresli()
                    self._vypis_zpravu(self._bojovnik_2.vrat_posledni_zpravu())
                    self._vypis_zpravu(self._bojovnik_1.vrat_posledni_zpravu())
    
    • Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.

    Opět vidíme, jak můžeme znovupoužívat kód. S dědičností je spojeno opravdu mnoho technik, jak si ušetřit práci. V našem případě to ušetří několik řádků, ale u většího projektu by to mohlo mít obrovský význam. Ostatní nesoukromé metody se automaticky podědí.

    Aplikace nyní funguje tak, jak má:

    Hra funguje správně:
    -------------- Aréna --------------
    
    Zdraví bojovníků:
    
    Gandalf [####################]                  ]
    Zalgoren [#############       ]
    Gandalf použil magii za 51 hp.
    Zalgoren utrpěl poškození 33 hp.

    Aréna nás však neinformuje o maně mága. Pojďme to napravit. Přidáme mágovi veřejnou metodu graficka_mana(), která bude obdobně jako u života vracet řetězec s grafickým ukazatelem many.

    Abychom nemuseli logiku se složením ukazatele psát dvakrát, upravíme metodu graficky_zivot() ve třídě Bojovnik. Připomeňme si, jak vypadá:

    def graficky_zivot(self):
        celkem = 20
        pocet = int(self._zivot / self._max_zivot * celkem)
        if (pocet == 0 and self.je_nazivu()):
            pocet = 1
        return f"[{'#' * pocet}{' ' * (celkem - pocet)}]"

    Vidíme, že není kromě proměnných zivot a max_zivot na životě nijak závislá. Metodu přejmenujeme na graficky_ukazatel() a dáme jí dva parametry: aktuální hodnotu a maximální hodnotu. Proměnné zivot a max_zivot v těle metody poté nahradíme za aktualni a maximalni:

    def graficky_ukazatel(self, aktualni, maximalni):
        celkem = 20
        pocet = int(aktualni / maximalni * celkem)
        if (pocet == 0 and self.je_nazivu()):
            pocet = 1
        return f"[{'#' * pocet}{' ' * (celkem - pocet)}]"

    Metodu graficky_zivot() ve třídě Bojovnik naimplementujeme znovu, bude nám v ní stačit jediný řádek a to zavolání metody graficky_ukazatel() s příslušnými parametry:

    def graficky_zivot(self):
        return self.graficky_ukazatel(self._zivot, self._max_zivot)

    Určitě jsme mohli v tutoriálu s bojovníkem udělat metodu graficky_ukazatel() rovnou. Chtěli jsme však ukázat, jak se řeší případy, kdy potřebujeme vykonat podobnou funkčnost vícekrát. S takovouto parametrizací se praxi setkáme často, protože nikdy přesně nevíme, co budeme v budoucnu od našeho programu požadovat.

    Nyní můžeme vykreslovat ukazatel tak, jak se nám to hodí. Přesuňme se do třídy Mag a naimplementujme metodu graficka_mana():

    def graficka_mana(self):
        return self.graficky_ukazatel(self._mana, self._max_mana)

    Jednoduché, že? Nyní je mág hotový, zbývá jen naučit arénu zobrazovat manu v případě, že je bojovník mág. Přesuňme se tedy do třídy Arena.

    Rozpoznání typu objektu

    Jelikož se nám nyní vykreslení bojovníka zkomplikovalo, uděláme si na něj samostatnou metodu vypis_bojovnika(). Jejím parametrem bude daná instance bojovníka:

    def _vypis_bojovnika(self, bojovnik):
        print(bojovnik)
        print(f"Život: {bojovnik.graficky_zivot()}")

    Nyní pojďme reagovat na to, jestli je bojovník mág. Už jsme si řekli, že k tomu slouží funkce isinstance():

    def _vypis_bojovnika(self, bojovnik):
        print(bojovnik)
        print(f"Život: {bojovnik.graficky_zivot()}")
        if isinstance(bojovnik, Mag):
            print(f"Mana: {bojovnik.graficka_mana()}")

    To bychom měli, vypis_bojovnika() budeme volat v metodě vykresli(), která bude vypadat takto:

    Klikni pro editaci
    • App
      • arena.py
      • main.py
      • kostka.py
      • bojovnik.py
      • mag.py
    •     def _vykresli(self):
             # zakomentováno kvůli kompileru
             # self._vycisti_obrazovku()
              print("-------------- Aréna -------------- \n")
              print("Bojovníci: \n")
              self._vypis_bojovnika(self._bojovnik_1)
              self._vypis_bojovnika(self._bojovnik_2)
      
      • Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.

      A máme hotovo :)

      Aplikaci ještě můžeme dodat hezčí vzhled. Vložíme například ASCIIart nadpis Aréna, který vytvoříme ASCII generátorem. Metodu k vykreslení ukazatele upravíme tak, aby vykreslovala plný obdélník místo # (ten napíšeme pomocí kláves Alt + 219). Výsledek bude vypadat takto:

      Upravená podoba aplikace:
         __    ____  ____  _  _    __
        /__\  (  _ \( ___)( \( )  /__\
       /(__)\  )   / )__)  )  (  /(__)\
      (__)(__)(_)\_)(____)(_)\_)(__)(__)
      
      Bojovníci:
      
      Zalgoren
      Život: ████
      
      Gandalf
      Život: ███████
      Mana:  █
      
      Gandalf použil magii za 48 hp
      Zalgoren utrpěl poškození 33 hp

      Kód je k dispozici v příloze. Pokud jste něčemu nerozuměli, zkuste si článek přečíst vícekrát nebo pomaleji, jsou to důležité praktiky.

      V následujícím kvízu, Kvíz - Dědičnost a polymorfismus v Pythonu, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.


       

      Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

      Stáhnout

      Stažením následujícího souboru souhlasíš s licenčními podmínkami

      Staženo 1274x (4.42 kB)
      Aplikace je včetně zdrojových kódů v jazyce Python

       

      Jak se ti líbí článek?
      Před uložením hodnocení, popiš prosím autorovi, co je špatněZnaků 0 z 50-500
      Předchozí článek
      Dědičnost a polymorfismus v Pythonu
      Všechny články v sekci
      Objektově orientované programování v Pythonu
      Přeskočit článek
      (nedoporučujeme)
      Kvíz - Dědičnost a polymorfismus v Pythonu
      Článek pro vás napsal gcx11
      Avatar
      Uživatelské hodnocení:
      635 hlasů
      (^_^)
      Aktivity