6. díl - C# - Aréna s bojovníky

C# .NET Objektově orientované programování C# - Aréna s bojovníky American English version English version

Unicorn College ONEbit hosting Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulém tutoriálu jsme si vytvořili třídu bojovníka. Hrací kostku máme hotovou z prvních lekcí objektově orientovaného programování. Dnes tedy dáme vše dohromady a vytvoříme funkční arénu. Tutoriál bude spíše oddechový a pomůže nám zopakovat si práci s objekty.

Potřebujeme napsat nějaký kód pro obsluhu bojovníků a výpis zpráv uživateli. Samozřejmě ho nebudeme bušit rovnou do Program.cs, ale vytvoříme si objekt Arena, kde se bude zápas odehrávat. Program.cs potom jen založí objekty a o zbytek se bude starat objekt Arena. Přidejme k projektu tedy poslední třídu a to Arena.cs.

Třída bude víceméně jednoduchá, jako atributy bude obsahovat 3 potřebné instance: 2 bojovníky a hrací kostku. V konstruktoru se tyto atributy naplní z parametrů. Kód třídy bude tedy následující (komentáře si dopiště):

class Arena
{
        private Bojovnik bojovnik1;
        private Bojovnik bojovnik2;
        private Kostka kostka;

        public Arena(Bojovnik bojovnik1, Bojovnik bojovnik2, Kostka kostka)
        {
                this.bojovnik1 = bojovnik1;
                this.bojovnik2 = bojovnik2;
                this.kostka = kostka;
        }

}

Zamysleme se nad metodami. Z veřejných metod bude určitě potřeba jen ta k simulaci zápasu. Výstup programu na konzoli uděláme trochu na úrovni a také umožníme třídě Arena, aby přímo ke konzoli přistupovala. Rozhodli jsme se, že výpis bude v kompetenci třídy, jelikož se nám to zde vyplatí. Naopak kdyby výpis prováděli i bojovníci, bylo by to na škodu (nebyli by univerzální). Potřebujeme tedy metodu, která vykreslí obrazovku s aktuálními údaji o kole a životy bojovníků. Zprávy o útoku a obraně budeme chtít vypisovat s dramatickou pauzou, aby byl výsledný efekt lepší, uděláme si pro takový typ zprávy ještě pomocnou metodu. Začněme s vykreslením informační obrazovky:

private void Vykresli()
{
        Console.Clear();
        Console.WriteLine("-------------- Aréna -------------- \n");
        Console.WriteLine("Zdraví bojovníků: \n");
        Console.WriteLine("{0} {1}", bojovnik1, bojovnik1.GrafickyZivot());
        Console.WriteLine("{0} {1}", bojovnik2, bojovnik2.GrafickyZivot());
}

Zde asi není co řešit, můžete si ještě obrazovku vyzdobit barevně, když budete chtít. Díky smazání konzole pomocí Clear() docílíme hezké informační obrazovky namísto klasického konzolového textu, kde se plná obrazovka roluje dolů. Metoda je privátní, budeme ji používat jen uvnitř třídy.

Další privátní metodou bude výpis zprávy s dramatickou pauzou:

private void VypisZpravu(string zprava)
{
        Console.WriteLine(zprava);
        Thread.Sleep(500);
}

Kód je zřejmý až na třídu Thread, která umožňuje práci s vlákny. My z ní využijeme pouze metodu Sleep(), která uspí vlákno programu na daný počet milisekund. S vlákny budeme pracovat až na konci seriálu. Aby vše fungovalo, musíme přidat using System.Threading; na začátek souboru Arena.cs.

Obě metody vlastně jen vypisují na konzoli, připadá mi zbytečné je zkoušet, přesuneme se tedy již k samotnému zápasu. Metoda Zapas() nebude mít žádné parametry a nebude ani nic vracet. Uvnitř bude cyklus, který bude na střídačku volat útoky bojovníků navzájem a vypisovat informační obrazovku a zprávy. Metoda by mohla vypadat takto:

public void Zapas()
{
        Console.WriteLine("Vítejte v aréně!");
        Console.WriteLine("Dnes se utkají {0} s {1}! \n", bojovnik1, bojovnik2);
        Console.WriteLine("Zápas může začít...");
        Console.ReadKey();
        // cyklus s bojem
        while (bojovnik1.Nazivu() && bojovnik2.Nazivu())
        {
                bojovnik1.Utoc(bojovnik2);
                Vykresli();
                VypisZpravu(bojovnik1.VratPosledniZpravu()); // zpráva o útoku
                VypisZpravu(bojovnik2.VratPosledniZpravu()); // zpráva o obraně
                bojovnik2.Utoc(bojovnik1);
                Vykresli();
                VypisZpravu(bojovnik2.VratPosledniZpravu()); // zpráva o útoku
                VypisZpravu(bojovnik1.VratPosledniZpravu()); // zpráva o obraně
                Console.WriteLine();
        }
}

Kód vypíše jednoduché informace a po stisku klávesy přejde do cyklu s bojem. Jedná se o while cyklus, který se opakuje, dokud jsou oba bojovníci naživu. První bojovník zaútočí na druhého, jeho útok vnitřně zavolá na druhém bojovníkovi obranu. Po útoku vykreslíme obrazovku s informacemi a dále zprávy o útoku a obraně pomocí naší metody VypisZpravu(), která po výpisu udělá dramatickou pauzu. To samé provedeme i pro druhého bojovníka.

Přesuňme se do Program.cs, vytvořme patřičné instance a zavolejme na aréně metodu Zapas():

// vytvoření objektů
Kostka kostka = new Kostka(10);
Bojovnik zalgoren = new Bojovnik("Zalgoren", 100, 20, 10, kostka);
Bojovnik shadow = new Bojovnik("Shadow", 60, 18, 15, kostka);
Arena arena = new Arena(zalgoren, shadow, kostka);
// zápas
arena.Zapas();
Console.ReadKey();

Charakteristiky hrdinů si můžete upravit dle libosti. Program spustíme:

Konzolová aplikace
-------------- Aréna --------------

Zdraví bojovníků:

Zalgoren [######              ]
Shadow [                    ]
Shadow útočí úderem za 20 hp
Zalgoren utrpěl poškození 4 hp

Výsledek je docela působivý. Objekty spolu komunikují, grafický život ubývá jak má, zážitek umocňuje dramatická pauza. Aréna má však 2 nedostatky.

  • V cyklu s bojem útočí první bojovník na druhého. Poté však vždy útočí i druhý bojovník, nehledě na to, zda ho první nezabil. Může tedy útočit již jako mrtvý. Podívejte se na screenshot výše, Shadow útočil jako poslední i když byl mrtvý. Až potom se vystoupilo z while cyklu. U prvního bojovníka tento problém není, u druhého musíme před útokem kontrolovat, zda je naživu.
  • Druhým nedostatkem je, že bojovníci vždy bojují ve stejném pořadí, čili zde "Zalgoren" má vždy výhodu. Pojďme vnést další prvek náhody a pomocí kostky rozhodněme, který z bojovníků bude začínat. Jelikož jsou bojovníci vždy dva, stačí hodit kostkou a podívat se, zda padlo číslo menší nebo rovné polovině počtu stěn kostky. Tedy např. pokud padne na desetistěnné kostce číslo do 5ti, začíná 2. bojovník, jinak začíná první. Zbývá zamyslet se nad tím, jak do kódu zanést prohazování bojovníků. Jistě by bylo velmi nepřehledné opodmínkovat příkazy ve while cyklu. Jelikož již víme, že v C# fungují reference, není pro nás problém udělat si 2 proměnné, ve kterých budou instance bojovníků, nazvěme je jednoduše b1 a b2. Do těchto proměnných si na začátku dosadíme bojovníky bojovnik1 a bojovnik2 tak, jak potřebujeme. Můžeme tedy při pozitivním hodu kostkou dosadit do b1 bojovník2 a naopak, výsledkem bude, že začínat bude ten druhý. Kód cyklu se takto vůbec nezmění a zůstane stále přehledný a jednoduchý, jen místo bojovnik bude b.

Změněná verze včetně podmínky, aby nemohl útočit mrtvý bojovník, by mohla vypadat nějak takto:

public void Zapas()
{
        // původní pořadí
        Bojovnik b1 = bojovnik1;
        Bojovnik b2 = bojovnik2;
        Console.WriteLine("Vítejte v aréně!");
        Console.WriteLine("Dnes se utkají {0} s {1}! \n", bojovnik1, bojovnik2);
        // prohození bojovníků
        bool zacinaBojovnik2 = (kostka.hod() <= kostka.VratPocetSten() / 2);
        if (zacinaBojovnik2)
        {
                b1 = bojovnik2;
                b2 = bojovnik1;
        }
        Console.WriteLine("Začínat bude bojovník {0}! \nZápas může začít...", b1);
        Console.ReadKey();
        // cyklus s bojem
        while (b1.Nazivu() && b2.Nazivu())
        {
                b1.Utoc(b2);
                Vykresli();
                VypisZpravu(b1.VratPosledniZpravu()); // zpráva o útoku
                VypisZpravu(b2.VratPosledniZpravu()); // zpráva o obraně
                if (b2.Nazivu())
                {
                        b2.Utoc(b1);
                        Vykresli();
                        VypisZpravu(b2.VratPosledniZpravu()); // zpráva o útoku
                        VypisZpravu(b1.VratPosledniZpravu()); // zpráva o obraně
                }
                Console.WriteLine();
        }
}

Program vyzkoušejme.

Konzolová aplikace
-------------- Aréna --------------

Zdraví bojovníků:

Zalgoren [#########           ]
Shadow [                    ]
Zalgoren útočí úderem za 27 hp
Shadow utrpěl poškození 11 hp a zemřel

Vidíme, že je vše již v pořádku. Gratuluji vám, pokud jste se dostali až sem a tutoriály opravdu četli a pochopili, máte základy objektového programování a dokážete tvořit rozumné aplikace :)

Příště se podíváme na objektově orientované programování podrobněji. V úvodu jsme si říkali, že OOP stojí na pilířích: zapouzdření, dědičnost a polymorfismus. První umíme již velmi dobře a modifikátor private je nám známý. Další dva nás čekají příště.


 

Stáhnout

Staženo 1167x (33.29 kB)
Aplikace je včetně zdrojových kódů v jazyce C#

 

 

Článek pro vás napsal David Čápka
Avatar
Jak se ti líbí článek?
23 hlasů
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.
Miniatura
Předchozí článek
Bojovník do arény
Miniatura
Následující článek
Dědičnost a polymorfismus
Aktivity (7)

 

 

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

Avatar
Libor Šimo (libcosenior):20.11.2016 20:55

Asi by to išlo aj takto:

b1.Nazivu() ? Console.WriteLine("Gratuluji k výhře bojovníkovi: {0} ", b1) : Console.WriteLine("Gratuluji k výhře bojovníkovi: {0} ", b2);

Ale nemám to otestované. Je to terárny operátor.

Editováno 20.11.2016 20:58
Odpovědět  +1 20.11.2016 20:55
Aj tisícmíľová cesta musí začať jednoduchým krokom.
Avatar
vajkuba1234
Člen
Avatar
Odpovídá na Libor Šimo (libcosenior)
vajkuba1234:20.11.2016 21:21

Libor Šimo (libcosenior) Jo, ale asi bych to radeji narval do metody, ta by porovnala zivoty a vratila by zpravu o vitezi. :-)

David Hanč

Editováno 20.11.2016 21:23
Odpovědět  +1 20.11.2016 21:21
No hope, no future, JUST WAR! For world peace Israel must be DESTROYED!
Avatar
Poggy
Člen
Avatar
Poggy:23. února 13:15

Děkuji moc za celý tento tutoriál o aréně. Skvěle napsáno. Polopatě a vždy krok po kroku. :-)

 
Odpovědět 23. února 13:15
Avatar
djand
Člen
Avatar
djand:13. března 21:03

Dobrý den, ze zvědavosti jsem v metodách Utoc() a BranSe() nahradil kód

kostka.hod()

kódem

random.Next(1, 11)

takže to teď vypadá takto:

Random random = new Random();

 public void Utoc(Bojovnik souper)
        {
            int uder = utok + random.Next(1, 11); // kostka.hod();
            NastavZpravu(String.Format("{0} útočí s úderem za {1} hp", jmeno, uder));
            souper.BranSe(uder);
        }
 public void BranSe(int uder)
        {
            int zraneni = uder - (obrana + random.Next(1, 11)); // kostka.hod());
            if (zraneni > 0) ....

...

bojovníkům jsem pak nastavil stejné hodnoty:

Bojovnik zalgoren = new Bojovnik("Zalgoren", 100, 20, 10, kostka);
Bojovnik gandalf = new Bojovnik("Gandalf", 100, 20, 10, kostka);

Výsledkem je že zranění je neustále 10 hp - nemění se to.
Nerozumím, proč tomu tak je... ??

 
Odpovědět 13. března 21:03
Avatar
djand
Člen
Avatar
Odpovídá na djand
djand:13. března 23:57

V dílu věnovanému hrací kostce jsem našel toto:

>

"Dejte si pozor, abyste netvořili generátor náhodných čísel v metodě, která má náhodné číslo vracet, tedy že by se pro každé náhodné číslo vytvořil nový generátor. Výsledná čísla pak nejsou téměř náhodná nebo dokonce vůbec. Vždy si vytvořte jednu sdílenou instanci generátoru (např. do privátního atributu pomocí konstruktoru) a na té potom metodu Next() volejte."

>
Ale moc tomu nerozumím...

 
Odpovědět 13. března 23:57
Avatar
Pavel Šrytr
Člen
Avatar
Odpovídá na djand
Pavel Šrytr:14. března 7:30

Měl by sis vytvořit privatní atribut a do něj v konstruktoru vložit instanci. Myslím že kdyby si instanci vytvořil vícekrát posobě generovala by podobná ne-li stejná čísla

 
Odpovědět  +1 14. března 7:30
Avatar
djand
Člen
Avatar
Odpovídá na Pavel Šrytr
djand:14. března 9:49

Jasně, to jsem pochopil. Nerozumím, proč tomu tak je. Kód:

random.Next(1, 11);

Problém je, že tenhle kód jsem chápal, jako "příkaz" náhodně vygeneruj číslo od 1 do 10.
Myslím, že např. v javaScriptu, to tak i je, ale tady to funguje jinak

 
Odpovědět 14. března 9:49
Avatar
Pavel Šrytr
Člen
Avatar
Pavel Šrytr:14. března 15:20

Pokud nechápeš vygenerování čísel (počítač si přece nemůže vymyslet číslo) Algoritmus Middle-square method tak, že počáteční hodnota(nastavena v konstruktoru nebo strojovým časem) se umocní ho nadruhou, vezme prostřední číslice a je tu "náhodné číslo"Například: 13 nadruhou je
169, prostřední číslo a zároveň náhodné číslo je 6. Pokud bude voláno vícekrát po sobě bude počáteční hodnota stejná a tak i výsledek.

 
Odpovědět 14. března 15:20
Avatar
Bruno Schwarzbach:19. března 11:35

]Prosím o radu. Pokouším se vytvořit algoritmus pro jednoduchou hru kámen-nůžky-papír. Snažil jsem se při tom myslet "objektově", ale něco dělám špatně. Mám třídu Hrac, pomocí proměnných jmeno1, jmeno2 vytvářím instance dvou hráčů v Mainu, ale potřebuji je mít se stejnými proměnnými (jmény hráčů) také ve třídě Hra. Nevím jak na to, aby algoritmus fungoval, musím je tam zadat tzv. natvrdo - viz řádky 11, 12 ve třídě hra. Díky předem za radu.

namespace kamen_nuzky
{
    enum Zbrane { kámen = 1, nůžky = 2, papír = 3 }
    class Program
    {
        static void Main(string[] args)
        {
            Zbrane Zbran1, Zbran2;
            string jmeno1 = "Petr";
            string jmeno2 = "Lucie";
            Hrac hrac1 = new Hrac(jmeno1);
            Hrac hrac2 = new Hrac(jmeno2);
            for (int i = 1; i < 10; i++)
            {
                Zbran1 = hrac1.ZvolZbran();
                Zbran2 = hrac2.ZvolZbran();
                Console.Write("{0} {1}", hrac1.jmeno, Zbran1);
                Console.WriteLine(" {0} {1}",hrac2.jmeno, Zbran2);
                Hra hra = new Hra(hrac1, hrac2, Zbran1, Zbran2);
                Console.WriteLine(hra.Vyhodnot());
                Console.ReadKey(true);
            }
        }
    }
}

namespace kamen_nuzky
{
    class Hrac
    {
        public string jmeno;
        public Zbrane zbran;

        private static Random random = new Random();

        public Hrac(string jmeno)
        {
            this.jmeno = jmeno;
        }
        public Zbrane ZvolZbran()
        {
            int nahVolba = random.Next(1, 4);
            return zbran = (Zbrane)nahVolba;
            //return string.Format("{0} dává {1}.", jmeno, zbran);
        }
    }
}

namespace kamen_nuzky
{
    class Hra
    {
        Hrac hrac1 = new Hrac("Petr"); //sem potřebuji propojení s proměnnou jmeno1
        Hrac hrac2 = new Hrac("Lucie");//sem potřebuji propojení s proměnnou jmeno2
        private Zbrane Zbran1;
        private Zbrane Zbran2;

        public Hra (Hrac hrac1, Hrac hrac2, Zbrane Zbran1, Zbrane Zbran2)
            {
            this.hrac1 = hrac1;
            this.hrac2 = hrac2;
            this.Zbran1 = Zbran1;
            this.Zbran2 = Zbran2;
        }
        public string Vyhodnot()
        {
            if (Zbran1 == Zbrane.kámen && Zbran2 == Zbrane.kámen)
                return "remíza";
            else if (Zbran1 == Zbrane.kámen && Zbran2 == Zbrane.nůžky)
                return hrac1.jmeno;
            else if (Zbran1 == Zbrane.kámen && Zbran2 == Zbrane.papír)
                return hrac2.jmeno;
            else if (Zbran1 == Zbrane.nůžky && Zbran2 == Zbrane.nůžky)
                return "remíza";
            else if (Zbran1 == Zbrane.nůžky && Zbran2 == Zbrane.kámen)
                return hrac2.jmeno;
            else if (Zbran1 == Zbrane.nůžky && Zbran2 == Zbrane.papír)
                return hrac1.jmeno;
            else if (Zbran1 == Zbrane.papír && Zbran2 == Zbrane.papír)
                return "remíza";
            else if (Zbran1 == Zbrane.papír && Zbran2 == Zbrane.kámen)
                return hrac1.jmeno;
            else if (Zbran1 == Zbrane.papír && Zbran2 == Zbrane.nůžky)
                return hrac2.jmeno;
            return "";
        }
    }
}

[/code
 
Odpovědět 19. března 11:35
Avatar
Bruno Schwarzbach:19. března 11:50
class Hra
    {
        //Hrac hrac1 = new Hrac("Petr"); //sem potřebuji propojení s proměnnou jmeno1
        //Hrac hrac2 = new Hrac("Lucie");//sem potřebuji propojení s proměnnou jmeno2
        private Zbrane Zbran1;
        private Zbrane Zbran2;
        private Hrac hrac1;
        private Hrac hrac2;

vyřešeno, beru dotaz zpět a omlouvám se

 
Odpovědět 19. března 11:50
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 40. Zobrazit vše