5. díl - Bojovník do arény v Pythonu

Python Objektově orientované programování Bojovník do arény v Pythonu

V minulém dílu seriálu o Pythonu jsme si vysvětlili rozdíly mezi referenčními a hodnotovými datovými typy. Již tedy víme, jak fungují reference a jak můžeme s objekty zacházet. Bude se nám to hodit dnes i příště. Tento a příští tutoriál budou totiž věnovány dokončení naší arény. Hrací kostku již máme, ještě nám chybí další 2 objekty: bojovník a samotná aréna. Dnes se budeme věnovat bojovníkovi. Nejprve si popišme, co má bojovník umět, poté se pustíme do psaní kódu.

Atributy

Bojovník se bude nějak jmenovat a bude mít určitý počet hp (tedy života, např. 80hp). Budeme uchovávat jeho maximální život (bude se lišit u každé instance) a jeho současný život, tedy např. zraněný bojovník bude mít 40hp z 80ti. Bojovník má určitý útok a obranu, obojí vyjádřené opět v hp. Když bojovník útočí s útokem 20hp na druhého bojovníka s obranou 10hp, ubere mu 10hp života. Bojovník bude mít referenci na instanci objektu Kostka. Při útoku či obraně si vždy hodí kostkou a k útoku/obraně přičte padlé číslo. (Samozřejmě by mohl mít každý bojovník svou kostku, ale chtěl jsem se přiblížit stolní podobě hry a ukázat, jak OOP opravdu simuluje realitu. Bojovníci tedy budou sdílet jednu instanci kostky.) Kostkou dodáme hře prvek náhody, v realitě se jedná vlastně o štěstí, jak se útok nebo obrana vydaří. Konečně budeme chtít, aby bojovníci podávali zprávy o tom, co se děje, protože jinak by z toho uživatel nic neměl. Zpráva bude vypadat např. "Zalgoren útočí s úderem za 25hp.". Zprávami se zatím nebudeme zatěžovat a vrátíme se k nim až nakonec.

Již víme, co budeme dělat, pojďme na to! :) Do souboru si přidejme třídu Bojovnik a dodejme ji patřičné atributy. Všechny budou privátní.

Metody

Pojďme pro atributy vytvořit konstruktor, nebude to nic těžkého. Komentář napíši jen sem, dále je vynechám. Nebudu je psát ani u dalších metod, aby se tutoriál zbytečně neroztahoval a zůstal přehledný. Vy si je samozřejmě dopište.

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

Všimněte si, že maximální zdraví si v konstruktoru nastavíme na zdraví. Předpokládáme, že je bojovník při vytvoření plně zdravý. Stačí nám tedy znát pouze jeho život a maximální život bude stejný.

Přejděme k metodám a opět se nejprve zamysleme nad tím, co by měl bojovník umět. Začněme tím jednodušším, budeme chtít nějakou textovou reprezentaci, abychom mohli bojovníka vypsat. Překryjeme tedy metodu __str__(), která vrátí jméno bojovníka. Určitě se nám bude hodit metoda, vracející zda je bojovník naživu. Aby to bylo trochu zajímavější, budeme chtít kreslit život bojovníka do konzole, nebudeme tedy psát, kolik má života, ale "vykreslíme" ho takto:

[#########    ]

Výše uvedený život by odpovídal asi 70%. Dosud zmíněné metody nepotřebovaly žádné parametry. Samotný útok a obranu nechme na později a pojďme si implementovat __str__(), nazivu() a graficky_zivot(). Začněme s __str__(), tam není co vymýšlet:

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

Sami si zkuste vytvořit metodu __repr__(). Nyní implementujme metodu nazivu(), opět to nebude nic těžkého. Stačí zkontrolovat, zda je život větší než 0 a podle toho se zachovat. Ovšem jestli je bojovník naživu by mohla být i jeho vlastnost. Python obsahuje specialitu - tzv. dekorátory. Dekorátory "vylepšují" metody. My využijeme dekorátor property, který mění metodu na vlastnost. Dekorátor se píše nad deklaraci funkce a před jeho název se píše zavináč.

@property
def nazivu(self):
    if self.__zivot > 0:
       return True
    else:
       return False

Jelikož i samotný výraz (self.__zivot > 0) je vlastně logická hodnota, můžeme vrátit tu a kód se značně zjednoduší:

@property
def nazivu(self):
    return self.__zivot > 0

Grafický život

Jak jsem se již zmínil, metoda graficky_zivot() bude umožňovat vykreslit ukazatel života v grafické podobě. Již víme, že z hlediska objektového návrhu není vhodné, aby metoda objektu přímo vypisovala do konzole (pokud není k výpisu objekt určený), proto si znaky uložíme do řetězce a ten vrátíme pro pozdější vypsání. Ukážeme si kód metody a následně podrobně popíšeme:

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

Určíme si celkovou délku ukazatele života do proměnné celkem (např. 20). Nyní v podstatě nepotřebujeme nic jiného, než trojčlenku. Pokud max_zivot odpovídá celkem dílků, zivot bude odpovídat pocet dílkům. Proměnná pocet je počet dílků aktuálního zdraví.

Matematicky platí, že pocet = (zivot / maxZivot) * celkem. My ještě doplníme přetypování na celé číslo.

Měli bychom ošetřit případ, kdy je život tak nízký, že nám vyjde na 0 dílků, ale bojovník je stále naživu. V tom případě vykreslíme 1 dílek, jinak by to vypadalo, že je již mrtvý. Dále využijeme formátování a replikování.

Vše si vyzkoušíme, přejděme na konec souboru a vytvořme si bojovníka (a kostku, protože tu musíme konstruktoru bojovníka předat). Následně vypišme, zda je naživu a jeho život graficky:

kostka = Kostka(10)
bojovnik = Bojovnik("Zalgoren", 100, 20, 10, kostka)
print("Bojovník: {0}".format(bojovnik)) #test __str__()
print("Naživu: {0}".format(bojovnik.nazivu)) #test naživu
print("Život: {0}".format(bojovnik.graficky_zivot())) #test graficky_zivot()
input()

A výsledek:

Stav bojovníka v Pythonu

Boj

Dostáváme se k samotnému boji. Implementujeme metody pro útok a obranu.

Obrana

Začněme obranou. Metoda bran_se() bude umožňovat bránit se úderu, jehož síla bude předána metodě jako parametr. Metodu si opět ukážeme a poté popíšeme:

def bran_se(self, uder):
    zraneni = uder - (self.__obrana + self.__kostka.hod())
    if zraneni > 0:
        self.__zivot = self.__zivot - zraneni
        if self.__zivot < 0:
            self.__zivot = 0

Nejprve spočítáme skutečné zranění a to tak, že z útoku nepřítele odečteme naši obranu zvýšenou o číslo, které padlo na hrací kostce. Pokud jsme zranění celé neodrazili (zraneni > 0), budeme snižovat náš život. Tato podmínka je důležitá, kdybychom zranění odrazili a bylo např. -2, bez podmínky by se život zvýšil. Po snížení života zkontrolujeme, zda není v záporné hodnotě a případně ho dorovnáme na nulu.

Útok

Metoda utoc() bude brát jako parametr instanci bojovníka, na kterého se útočí. To proto, abychom na něm mohli zavolat metodu bran_se(), která na náš útok zareaguje a zmenší protivníkův život. Zde vidíme výhody referencí v Pythonu, můžeme si instance jednoduše předávat a volat na nich metody, aniž by došlo k jejich zkopírování. Jako první vypočteme úder, podobně jako při obraně, úder bude náš útok + hodnota z hrací kostky. Na soupeři následně zavoláme metodu bran_se() s hodnotou úderu:

uder = utok + kostka.hod()
souper.bran_se(uder)

To bychom měli, pojďme si zkusit v našem ukázkovém programu zaútočit a poté znovu vykreslit život. Pro jednoduchost nemusíme zakládat dalšího bojovníka, ale můžeme zaútočit sami na sebe:

kostka = Kostka(10)
bojovnik = Bojovnik("Zalgoren", 100, 20, 10, kostka)
print("Bojovník: {0}".format(bojovnik)) #test __str__()
print("Naživu: {0}".format(bojovnik.nazivu)) #test naživu
print("Život: {0}".format(bojovnik.graficky_zivot())) #test graficky_zivot()
bojovnik.utoc(bojovnik)
print("Život po útoku: {0}".format(bojovnik.graficky_zivot()))
input()
Stav bojovníka v Pythonu

Zdá se, že vše funguje, jak má. Přejděme k poslednímu bodu dnešního tutoriálu a to ke zprávám.

Zprávy

Jak již bylo řečeno, o útocích a obraně budeme uživatele informovat výpisem na konzoli. Výpis nebude provádět samotná třída Bojovnik, ta bude jen vracet zprávy jako textové řetězce. Jedna možnost by byla při volání metod utoc() a bran_se() vrátit i zprávu. Problém by však nastal v případě, když bychom chtěli získat zprávu od metody, která již něco vrací. To by nebylo moc hezké řešení.

Pojďme na věc univerzálněji, zprávu budeme ukládat do privátní proměnné zprava a uděláme metody pro její uložení a navrácení. Samozřejmě bychom mohli udělat proměnnou veřejnou, ale není zde důvod, proč umožnit zvenčí zápis do zprávy a také by skládání složitější zprávy uvnitř třídy mohlo být někdy problematické.

Do metody __init__() tedy přidáme:

self.__zprava = ""

Nyní si vytvoříme dvě metody. Privátní __nastav_zpravu(), která bere jako parametr text zprávy a slouží pro vnitřní účely třídy, kde nastaví zprávu do privátní proměnné. Všimněte si, že soukromé metody také začínají se dvěma podtržítky.

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

Nic složitého. Podobně jednoduchá bude veřejná metoda pro navrácení zprávy:

def vrat_posledni_zpravu(self):
    return self.__zprava

O práci se zprávami obohatíme naše metody utoc() a bran_se(), nyní budou vypadat takto:

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:
            self.__zivot = 0
            zprava = zprava[:-1] + " a zemřel."
    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)

Vše si opět vyzkoušíme, tentokrát již vytvoříme druhého bojovníka:

kostka = Kostka(10)
bojovnik = Bojovnik("Zalgoren", 100, 20, 10, kostka)
print("Život: {0}".format(bojovnik.graficky_zivot())) #test graficky_zivot()
#útok na našeho bojovníka
souper = Bojovnik("Shadow", 60, 18, 15, kostka)
souper.utoc(bojovnik)
print(souper.vrat_posledni_zpravu())
print(bojovnik.vrat_posledni_zpravu())
print("Život: {0}".format(bojovnik.graficky_zivot()))
input()
Stav bojovníka v Pythonu

Máme kostku i bojovníka, teď již chybí jen aréna, tu si vytvoříme hned příště.


 

Stáhnout

Staženo 165x (4.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


 



 

 

Komentáře
Zobrazit starší komentáře (2)

Avatar
Tomáš Pařízek:

Nedal byste mi někdo kompletní zdroják?
Někde dělám chybu a nevím kde.

 
Odpovědět 30. července 12:49
Avatar
gcx11
Redaktor
Avatar
Odpovídá na Tomáš Pařízek
gcx11:

Tak jsem ho hledal, jestli ho ještě nemám a nakonec ho mám, ale došlo mi, že u článku je přesně ten samý zdrojový kód ke stažení, takže si to asi stáhni odsud.

 
Odpovědět 30. července 13:05
Avatar
Odpovídá na gcx11
Tomáš Pařízek:

Děkuji, sice mě to mohlo napadnout ale nenapadlo.
Je to super, že zde máme takové redaktory.

 
Odpovědět 30. července 13:08
Avatar
kampkin
Člen
Avatar
kampkin:

Zdravím,
ošetření grafických životů proti stavu " [ ]", když je bojovník stále naživu se mi zdá elegantnější za pomoci knihovny math, konkrétně metody ceil, která zaokrouhlí desetinná čísla nahoru. :)

def graficky_zivot(self, celkem = 20):
    import math
    pocet = math.ceil(self.__zivot / self.__max_zivot * celkem)
    return "[{0}{1}]".format("#"*pocet, " "*(celkem-pocet))
 
Odpovědět 10. srpna 12:37
Avatar
gcx11
Redaktor
Avatar
Odpovídá na kampkin
gcx11:

Ahoj, tohle se zdá jako lepší řešení, ale podle konvencí by se měly importy dávat na začátek skriptu. PEP 8

Editováno 10. srpna 12:59
 
Odpovědět 10. srpna 12:59
Avatar
kampkin
Člen
Avatar
Odpovídá na gcx11
kampkin:

Děkuji za upozornění, příště si dám pozor :)
pouze jsem se inpiroval při vytváření kostky, přesněji import knihovny random

def hod(self):
    """Vrátí náhodné číslo od 1 do počtu stěn"""
    import random as _random
    return _random.randint(1, self.__pocet_sten)

Je tento zápis v pořádku nebo bychom měli import také provést na začátku?
Popřípadě import je vhodnější provést na úplném začátku skriptu anebo v metodě __init__()?
Předem děkuji za odpověď

 
Odpovědět 10. srpna 13:25
Avatar
gcx11
Redaktor
Avatar
Odpovídá na kampkin
gcx11:

Teď už bych to asi takhle nepsal. Také by se to mělo dát na úplný začátek skriptu, ale až pod shebang, to je to #!/usr/bin/env python3. Metoda __init__ slouží k inicializaci objektu, tam bych to neimportoval už vůbec, jelikož by to nefungovalo a akorát vyhodilo chybu.

class Foo:
        def __init__(self):
                import math
                this.x = 4
        def bar(self):
                return math.sqrt(x)

foo = Foo()
foo.bar() # vyhodí chybu, protože bar tu math nezná
 
Odpovědět 10. srpna 13:35
Avatar
gcx11
Redaktor
Avatar
Odpovídá na kampkin
gcx11:

Jelikož Python v té funkci musí mít nadefinovaný to jméno math a takhle ho nemá. Ale tohle by fungovalo, ovšem je to strašná prasarána.

#!/usr/bin/env python3

class Foo:

    def __init__(self):
        self.math = __import__("math")
        self.x = 4

    def bar(self):
        return self.math.sqrt(self.x)

foo = Foo()
print(foo.bar())

A nahoře mám chybu, to this má být self, už se ten C# začal projevovat :D

Editováno 10. srpna 13:46
 
Odpovědět 10. srpna 13:45
Avatar
kampkin
Člen
Avatar
kampkin:

Děkuji mnohokrát :D

 
Odpovědět 10. srpna 13:53
Avatar
Daniel Martinek:

Ahoj, myslim ze u Utoku kdyz pises

uder = utok + kostka.hod()
souper.bran_se(uder)

Tak potom zkouska nefunguje.
Ve zdrojaku hry ke stazeni to je uz v pohode.

 
Odpovědět 2. prosince 17:28
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 10 zpráv z 12. Zobrazit vše