8. díl - Aréna s mágem (dědičnost a polymorfismus)

Python Objektově orientované programování Aréna s mágem (dědičnost a polymorfismus)

V minulém dílu tutoriálů o Python jsme si vysvětlili dědičnost a polymorfismus. Dnes máme slíbeno, že si je vyzkoušíme v praxi. Bude to opět na naší aréně, 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 nějaké své 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

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šší damage, 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.

Vytvoříme tedy třídu Mag, zdědíme ji z Bojovnik. Zatím bude vypadat takto (opět si ji okomentujte):

class Mag(Bojovnik):

Jelikož v mágovi nebudeme mít přístup ke všem proměnným, protože jsou v bojovníkovi nastavené jako soukromé, tak musíme třídu Bojovnik upravit. Změníme atributy ze soukromých na vnitřní. Musíme je ale změnit ve všech metodách. Budeme potřebovat jen kostka a jmeno, ale klidně takto nastavíme všechny atributy charakteru, protože se v budoucnu mohou hodit, kdybychom se rozhodli podědit další typy bojovníků. Naopak atribut zprava není vhodné nastavovat jako vnitřní, protože nesouvisí s bojovníkem, ale s nějakou vnitřní logikou třídy. Upravený konstruktor bojovníka tedy bude vypadat nějak takto:

...
    def __init__(self, jmeno, zivot, utok, obrana, kostka):
        self._jmeno = jmeno
        self._zivot = zivot
        self._max_zivot = zivot
        self._utok = utok
        self._obrana = obrana
        self._kostka = kostka
        self.__zprava = ""
...

Přejděme ke konstruktoru mága.

Konstruktor potomka

Původní konstruktor bojovníka použít nemůžeme, neboť máme u mága 2 parametry navíc (mana 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.

O 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 se volání konstruktoru předka (metody super()) píše do těla metody.

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

Pozn. 1 - V Pythonu 2.X by byla syntaxe metody super() následující: super(Mag, self).__init__(...) Můžeme ale použít i zkrácenou verzi pro konstruktor a to: super(...)

Pozn. 2 - Stejně můžeme volat i jinou metodu v té samé třídě (ne předka), jen místo super() použijeme self.

Přesuňme se na konec programu a druhého bojovníka (Shadow) změňme na mága, např. takto:

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

Změnu samozřejmě musíme 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 objekt Arena mohl 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 objektu 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. Přepíšeme zděděnou metodu tak, aby útok pracoval s manou, hlavička metody však 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í.

A když jsme u metod, budeme ještě jistě používat metodu __nastav_zpravu(), ta je však soukromá. Označme ji jako vnitřní:

def _nastav_zpravu(self, zprava):
    ...

Pozn. Při návrhu bojovníka jsme samozřejmě měli myslet na to, že se z něj bude dědit a již označit vhodné atributy a metody jako vnitřní. V tutoriálu k bojovníkovi jsem vás tím však nechtěl zbytečně zatěžovat, proto musíme modifikátory změnit až teď, kdy jim rozumíme :)

Nyní se vraťme k metodě utoc() a přepíšeme (překryjeme) jí. 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 = "{0} útočí s úderem za {1} hp.".format(self._jmeno, uder)
        self._nastav_zpravu(zprava)
    #magický útok
    else:
        uder = self.__magicky_utok + self._kostka.hod()
        zprava = "{0} použil magii za {1} hp.".format(self._jmeno, uder)
        self._nastav_zpravu(zprava)
        self.__mana = 0
    souper.bran_se(uder)

Kód je asi srozumitelný, všimněte si omezení many na max_mana. Může se nám totiž stát, že tuto hodnotu přesáhneme, když ji zvyšujeme o 10. Když se nad kódem zamyslíme, tak útok výše v podstatě vykonává původní metoda utoc(). Jistě by bylo přínosné zavolat podobu metody na předkovi místo toho, abychom chování opisovali. K tomu opět použijeme super():

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 = "{0} použil magii za {1} hp.".format(self._jmeno, uder)
        self._nastav_zpravu(zprava)
        self.__mana = 0
        souper.bran_se(uder)

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á.

Tahová hra aréna s mágem v Pythonu

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.nazivu):
        pocet = 1
    return "[{0}{1}]".format("#"*pocet, " "*(celkem-pocet))

Vidíme, že není kromě proměnných zivot a max_zivot na životě nijak závislá. Metodu přejmenujeme na GrafickyUkazatel a dáme ji 2 parametry: aktuální hodnotu a maximální hodnotu. 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.nazivu):
        pocet = 1
    return "[{0}{1}]".format("#"*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ě jsem mohl v tutoriálu s bojovníkem udělat metodu graficky_ukazatel() rovnou. Chtěl jsem však, abychom si ukázali, jak se řeší případy, kdy potřebujeme vykonat podobnou funkčnost vícekrát. S takovouto parametrizací se v praxi budete setkávat č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("Život: {0}".format(bojovnik.graficky_zivot()))

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

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

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

def __vykresli(self):
    self.__vycisti_obrazovku()
    print("-------------- Aréna -------------- \n")
    print("Bojovníci: \n")
    self.__vypis_bojovnika(self.__bojovnik_1)
    self.__vypis_bojovnika(self.__bojovnik_2)
    print("")

Hotovo :)

Tahová hra aréna s mágem v Pythonu

Kód máte 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. Příště si vysvětlíme pojem statika.


 

Stáhnout

Staženo 132x (8.21 kB)
Aplikace je včetně zdrojových kódů v jazyce python

 

  Aktivity (1)

Článek pro vás napsal gcx11
Avatar
(^_^)

Jak se ti líbí článek?
Celkem (3 hlasů) :
55555


 


Miniatura
Následující článek
Statika v Pythonu

 

 

Komentáře

Avatar
Daniel Martinek:

Ahoj, malinko jsem si to vylepšil.
Přidal jsem si do třídy Bojovnik "duše" - každý má 3 duše, když umře, tak 1 ztratí.
Je to takhle v pořádku, nebo by jsi to udelal jinak ?

class Bojovnik:
    """
    Třída reprezentující bojovníka do arény.
    """

    def __init__(self, jmeno, zivot, utok, obrana, duse, kostka):

        self._jmeno = jmeno
        self._zivot = zivot
        self._max_zivot = zivot
        self._utok = utok
        self._obrana = obrana
        self._duse = duse
        self._kostka = kostka
        self.__zprava = ""

    def __str__(self):
        return str(self._jmeno)

    def __repr__(self):
        return str("Bojovnik({0}, {1}, {2}, {3}, {4})".format(self._jmeno,
                                                              self._max_zivot,
                                                              self._utok,
                                                              self._obrana,
                                                              self._kostka))

    @property
    def nazivu(self):
        """
        Vrátí True, pokud je bojovník naživu.
        Jinak vrátí False.
        """
        return self._zivot > 0

    def graficky_zivot(self):
        celkem = 20
        pocet = int(self._zivot / self._max_zivot * celkem)
        if(pocet == 0 and self.nazivu):
            pocet = 1
        return "[{0}{1}]       Duše: {2}".format("#"*pocet, " "*(celkem-pocet),
                                                  self._duse)
    def bran_se(self, uder):
        zraneni = uder - (self._obrana + self._kostka.hod())
        if zraneni > 0:
            zprava = "{0} utrpěl poškození {1} hp.".format(self._jmeno,
                                                           zraneni)
            self._zivot = self._zivot - zraneni
            if self._zivot <= 0:
                if self._duse == 1:
                    self._duse = self._duse - 1
                    self._zivot = 0
                    zprava = zprava[:-1] + " a zemřel."
                else:
                    self._duse = self._duse - 1
                    self._zivot = self._max_zivot
        else:
            zprava = "{0} odrazil útok.".format(self._jmeno)
        self._nastav_zpravu(zprava)

    def utoc(self, souper):
        uder = self._utok + self._kostka.hod()
        zprava = "{0} útočí s úderem za {1} hp.".format(self._jmeno, uder)
        self._nastav_zpravu(zprava)
        souper.bran_se(uder)

    def _nastav_zpravu(self, zprava):
        self.__zprava = zprava

    def vrat_posledni_zpravu(self):
        return self.__zprava

    def __nastav_duse(self, duse):
        if self._zivot == 0:
            souper.__duse = souper._duse + 1

    def konec_hry(self):
        return self._duse < 5

Dal jen duse Mag vydedil a pri vytvareni jsou nastaveny duse na "3":

zalgoren = Bojovnik("Zalgoren", 50, 20, 10, 3, kostka)
gandalf = Mag("Gandalf ", 40, 15, 12, 3, kostka, 30, 45)

Diky moc a omlouvam se za dlouhy zdrojak... :D

Editováno 3. prosince 22:09
 
Odpovědět 3. prosince 22:07
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 1 zpráv z 1.