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 9 - Bojovník do arény v Pythonu

V předešlém cvičení, Řešené úlohy k 7.-8. lekci OOP v Pythonu, jsme si procvičili nabyté zkušenosti z předchozích lekcí.

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í Python tutoriál budou věnovány dokončení naší arény. Hrací kostku již máme, ještě nám chybí další dva 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 bodů ž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 80. 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! 🙂 Nejprve upravíme strukturu našeho projektu TahovyBoj. Třídu Kostka v souboru kostka.py máme hotovou a dál se jí nebudeme zabývat, jen ji budeme používat. Aby to bylo možné, budeme si ji muset do všech dalších souborů, kde ji chceme využít, nejprve importovat. V našem případě to bude import do nového souboru main.py, který si založíme. V něm bude hlavní kód programu, tedy to, co budeme psát mimo třídy. Arénu budeme mít v souboru arena.py. Ten si jen založíme a necháme na příště. Strukturu projektu tím máme hotovou a nic nám nebrání pustit se do bojovníka. Do nového souboru bojovnik.py si tedy přidejme třídu Bojovnik. Za moment ji dodáme i patřičné atributy. Všechny budou privátní. Třída Bojovnik zatím vypadá tedy takto:

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

Metody

Pojďme pro atributy vytvořit konstruktor, nebude to nic těžkého. Komentáře zde vynechám, vy si je dopište podobně, jako u atributů výše. Nebudu je psát ani u dalších metod, aby se tutoriál zbytečně neroztahoval a zůstal přehledný (případně se podívejte do archivu):

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

Všimněme si, že maximální zdraví si v konstruktoru odvodíme a nemáme na něj parametr v hlavičce. 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 metody __str__(), je_nazivu() a graficky_zivot(). Začněme se __str__(), tam není co vymýšlet:

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

Nyní implementujme metodu je_nazivu(), opět to nebude nic těžkého. Stačí zkontrolovat, zda je život větší než 0 a podle toho se zachovat:

def je_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ší:

def je_nazivu(self):
    return self._zivot > 0

Grafický život

Jak už jsme zmínili, metoda graficky_zivot() umožní 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í. Podívejme se na kód metody a následně si jej podrobně popíšeme:

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)}]"

Určíme si celkovou délku ukazatele života do proměnné celkem (například 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 jeden 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 do souboru main.py a importujme si třídy Bojovnik a Kostka:

from bojovnik import Bojovnik
from kostka import Kostka

Dále si vytvoříme instanci bojovníka (a kostky, protože tu musíme konstruktoru bojovníka předat). Následně vypíšeme, zda je naživu a jeho život graficky:

Klikni pro editaci
  • App
    • main.py
    • bojovnik.py
    • kostka.py
  • from kostka import Kostka
    from bojovnik import Bojovnik
    
    kostka = Kostka(10)
    bojovnik = Bojovnik("Zalgoren", 100, 20, 10, kostka)
    print(f"Bojovník: {bojovnik}")  # test __str__()
    print(f"Naživu: {bojovnik.je_nazivu()}")  # test naživu
    print(f"Život: {bojovnik.graficky_zivot()}")  # test graficky_zivot()
    
  • 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
    
        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)}]"
    
  • class Kostka:
        """
        Třída reprezentuje hrací kostku.
        """
    
        def __init__(self,pocet_sten=6):
            self._pocet_sten = pocet_sten
    
        def vrat_pocet_sten(self):
            """
            Vrátí počet stěn kostky.
            """
            return self._pocet_sten
    
        def hod(self):
            """
            Vykoná hod kostkou a vrátí číslo od 1 do
            počtu stěn.
            """
            import random as _random
            return _random.randint(1, self._pocet_sten)
    
        def __str__(self):
            """
            Vrací textovou reprezentaci kostky.
            """
            return f"Kostka({self._pocet_sten})"
    
    • Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.

    Podívejme se na výstup:

    Zalgoren žije!
    Bojovník: Zalgoren
    Naživu: True
    Život: [####################]

    Boj

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

    Obrana

    Začněme obranou. Vrátíme se do souboru bojovnik.py a do třídy Bojovnik přidáme obrannou metodu. 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říklad -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ě bude úder tvořit náš útok + hodnota z hrací kostky. Na soupeři následně zavoláme metodu bran_se() s hodnotou úderu:

    def utoc(self, souper):
        uder = self._utok + self._kostka.hod()
        souper.bran_se(uder)

    To bychom měli, pojďme si zkusit v našem hlavním programu (v main.py) zaútočit a poté znovu vykreslit život. Pro jednoduchost nebudeme zakládat dalšího bojovníka, ale zaútočíme sami na sebe:

    Klikni pro editaci
    • App
      • main.py
      • bojovnik.py
      • kostka.py
    • from kostka import Kostka
      from bojovnik import Bojovnik
      
      kostka = Kostka(10)
      bojovnik = Bojovnik("Zalgoren", 100, 20, 10, kostka)
      print(f"Bojovník: {bojovnik}")  # test __str__()
      print(f"Naživu: {bojovnik.je_nazivu()}")  # test naživu
      print(f"Život: {bojovnik.graficky_zivot()}")  # test graficky_zivot()
      bojovnik.utoc(bojovnik)
      print(f"Život po útoku: {bojovnik.graficky_zivot()}")
      
    • 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
      
          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 "[{0}{1}]".format("#"*pocet, " "*(celkem-pocet))
      
          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
      
          def utoc(self, souper):
              uder = self._utok + self._kostka.hod()
              souper.bran_se(uder)
      
    • class Kostka:
          """
          Třída reprezentuje hrací kostku.
          """
      
          def __init__(self,pocet_sten=6):
              self._pocet_sten = pocet_sten
      
          def vrat_pocet_sten(self):
              """
              Vrátí počet stěn kostky.
              """
              return self._pocet_sten
      
          def hod(self):
              """
              Vykoná hod kostkou a vrátí číslo od 1 do
              počtu stěn.
              """
              import random as _random
              return _random.randint(1, self._pocet_sten)
      
          def __str__(self):
              """
              Vrací textovou reprezentaci kostky.
              """
              return f"Kostka({self._pocet_sten})"
      
      • Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.

      Vše funguje, jak má:

      Zalgoren se zranil!
      Bojovník: Zalgoren
      Naživu: True
      Život: [####################]
      Život po útoku: [#################   ]

      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í. Metoda samozřejmě nemůže jednoduše vrátit dvě věci.

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

      Vždy je lepší být "přehnaně" důsledný a využívat zapouzdření všude, kde je to možné.

      Do metody konstruktoru bojovníka __init__() tedy přidáme:

      self._zprava = ""

      Nyní si ve třídě Bojovník vytvoříme dvě metody. Privátní metodu _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é:

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

      Vše si opět vyzkoušíme v main.py. Tentokrát již vytvoříme druhého bojovníka:

      Klikni pro editaci
      • App
        • main.py
        • bojovnik.py
        • kostka.py
      • from kostka import Kostka
        from bojovnik import Bojovnik
        
        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()))
        
      • 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
        
      • class Kostka:
            """
            Třída reprezentuje hrací kostku.
            """
        
            def __init__(self,pocet_sten=6):
                self._pocet_sten = pocet_sten
        
            def vrat_pocet_sten(self):
                """
                Vrátí počet stěn kostky.
                """
                return self._pocet_sten
        
            def hod(self):
                """
                Vykoná hod kostkou a vrátí číslo od 1 do
                počtu stěn.
                """
                import random as _random
                return _random.randint(1, self._pocet_sten)
        
            def __str__(self):
                """
                Vrací textovou reprezentaci kostky.
                """
                return f"Kostka({self._pocet_sten})"
        
        • Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.

        Podívejme se na výsledek:

        Shadow útočí!
        Život: [####################]
        Shadow útočí s úderem za 24 hp.
        Zalgoren utrpěl poškození 7 hp.
        Život: [##################  ]

        To je k dnešní lekci vše :-)

        V další lekci, Aréna s bojovníky v Pythonu. dokončíme arénu simulující zápas dvou bojovníků.


         

        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 1226x (5.25 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
        Řešené úlohy k 7.-8. lekci OOP v Pythonu
        Všechny články v sekci
        Objektově orientované programování v Pythonu
        Přeskočit článek
        (nedoporučujeme)
        Aréna s bojovníky v Pythonu
        Článek pro vás napsal gcx11
        Avatar
        Uživatelské hodnocení:
        682 hlasů
        (^_^)
        Aktivity