IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
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 11 - 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:

    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())

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:

    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)

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 1045x (4.42 kB)
Aplikace je včetně zdrojových kódů v jazyce Python

 

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í:
375 hlasů
(^_^)
Aktivity