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 Ťahový boj, 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.
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 TahovyBoj
vytvoríme teda triedu
Mag
, ktorú zdedíme z triedy Bojovnik
. Opäť si pre
ňu založíme samostatný súbor a nazveme ho mag.py
Obsah súboru
bude zatiaľ vyzerať takto (triedu si opäť okomentujte):
from bojovnik import Bojovnik class Mag(Bojovnik):
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, 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
Presuňme sa do hlavného programu v main.py
a druhého
bojovníka (Shadow) zmeňme na mága:
gandalf = Mag("Gandalf", 60, 15, 12, kostka, 30, 45)
Samozrejme musíme doplniť import triedy Mag
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 utoc()
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
utoc()
z predka. Zdedenú metódu prepíšeme tak, aby útok
pracoval s manom. 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 utoc()
. V súbore s mágom
(mag.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 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 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 utoc()
. 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 utoc()
tak, ako je definovaná v
rodičovskej triede Bojovnik
:
{PYTHON} from bojovnik import Bojovnik class Mag(Bojovnik): 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 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) {/PYTHON}
{PYTHON} class Bojovnik: """ Třída reprezentující bojovníka do arény. """ def __init__(self, jmeno, zivot, utok, obrana, kostka): """ jmeno - jméno bojovníka zivot - maximální život bojovníka utok - útok bojovníka obrana - obrana bojovníka kostka - instance kostky """ self._jmeno = jmeno self._zivot = zivot self._max_zivot = zivot self._utok = utok self._obrana = obrana self._kostka = kostka self._zprava = "" def __str__(self): return str(self._jmeno) def je_nazivu(self): return self._zivot > 0 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)}]" def bran_se(self, uder): zraneni = uder - (self._obrana + self._kostka.hod()) if zraneni > 0: zprava = f"{self._jmeno} utrpěl poškození {zraneni} hp." self._zivot = self._zivot - zraneni if self._zivot < 0: self._zivot = 0 zprava = f"{zprava[:-1]} a zemřel." else: zprava = f"{self._jmeno} odrazil útok." self._nastav_zpravu(zprava) def utoc(self, souper): uder = self._utok + self._kostka.hod() zprava = f"{self._jmeno} útočí s úderem za {uder} hp." self._nastav_zpravu(zprava) souper.bran_se(uder) def _nastav_zpravu(self, zprava): self._zprava = zprava def vrat_posledni_zpravu(self): return self._zprava {/PYTHON}
{PYTHON} 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() {/PYTHON}
{PYTHON} 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})" {/PYTHON}
{PYTHON} 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()) {/PYTHON}
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á:
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 mane mága. Poďme to napraviť. Pridáme
mágovi verejnú metódu graficka_mana()
, 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 graficky_zivot()
v triede Bojovnik
.
Pripomeňme si, ako vyzerá:
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 nie je okrem premenných zivot
a
max_zivot
na živote nijako závislá. Metódu premenujeme na
graficky_ukazatel()
a dáme jej dva parametre: aktuálnu hodnotu a
maximálnu hodnotu. Premenné zivot
a max_zivot
v tele
metódy potom 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)}]"
Metódu graficky_zivot()
v triede Bojovnik
naimplementujeme znova, bude nám v nej stačiť jediný riadok a to zavolanie
metódy graficky_ukazatel()
s príslušnými parametrami:
def graficky_zivot(self): return self.graficky_ukazatel(self._zivot, self._max_zivot)
Určite sme mohli v tutoriáli s bojovníkom urobiť metódu
graficky_ukazatel()
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 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 Mag
a naimplementujme metódu
graficka_mana()
:
def graficka_mana(self): return self.graficky_ukazatel(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ň
samostatnú metódu vypis_bojovnika()
. Jej parametrom bude daná
inštancia bojovníka:
def _vypis_bojovnika(self, bojovnik): print(bojovnik) print(f"Život: {bojovnik.graficky_zivot()}")
Teraz poďme reagovať na to, či je bojovník mág. Už sme si povedali, že
na to slúži funkcia 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 by sme mali, vypis_bojovnika()
budeme volať v metóde
vykresli()
, ktorá bude vyzerať takto:
{PYTHON} 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("Bojovníci: \n") self._vypis_bojovnika(self._bojovnik_1) self._vypis_bojovnika(self._bojovnik_2) """ 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) _time.sleep(.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()) def _vypis_bojovnika(self, bojovnik): print(bojovnik) print(f"Život: {bojovnik.graficky_zivot()}") if isinstance(bojovnik, Mag): print(f"Mana: {bojovnik.graficka_mana()}") {/PYTHON}
{PYTHON} 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() {/PYTHON}
{PYTHON} 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})" {/PYTHON}
{PYTHON} class Bojovnik: """ Třída reprezentující bojovníka do arény. """ def __init__(self, jmeno, zivot, utok, obrana, kostka): """ jmeno - jméno bojovníka zivot - maximální život bojovníka utok - útok bojovníka obrana - obrana bojovníka kostka - instance kostky """ self._jmeno = jmeno self._zivot = zivot self._max_zivot = zivot self._utok = utok self._obrana = obrana self._kostka = kostka self._zprava = "" def __str__(self): return str(self._jmeno) def je_nazivu(self): return self._zivot > 0 def graficky_zivot(self): return self.graficky_ukazatel(self._zivot, self._max_zivot) 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)}]" def bran_se(self, uder): zraneni = uder - (self._obrana + self._kostka.hod()) if zraneni > 0: zprava = f"{self._jmeno} utrpěl poškození {zraneni} hp." self._zivot = self._zivot - zraneni if self._zivot < 0: self._zivot = 0 zprava = f"{zprava[:-1]} a zemřel." else: zprava = f"{self._jmeno} odrazil útok." self._nastav_zpravu(zprava) def utoc(self, souper): uder = self._utok + self._kostka.hod() zprava = f"{self._jmeno} útočí s úderem za {uder} hp." self._nastav_zpravu(zprava) souper.bran_se(uder) def _nastav_zpravu(self, zprava): self._zprava = zprava def vrat_posledni_zpravu(self): return self._zprava {/PYTHON}
{PYTHON} from bojovnik import Bojovnik class Mag(Bojovnik): 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 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) def graficka_mana(self): return self.graficky_ukazatel(self._mana, self._max_mana) {/PYTHON}
A máme hotovo:)
Aplikáciu ešte môžeme dodať krajší vzhľad. Vložíme napríklad
ASCIIart nadpis Aréna, 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:
Upravená podoba aplikace:
__ ____ ____ _ _ __
/__\ ( _ \( ___)( \( ) /__\
/(__)\ ) / )__) ) ( /(__)\
(__)(__)(_)\_)(____)(_)\_)(__)(__)
Bojovníci:
Zalgoren
Život: ████
Gandalf
Život: ███████
Mana: █
Gandalf použil mágiu za 48 hp
Zalgoren utrpel poškodenie 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é 1111x (4.42 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Python