Lekce 6 - Aréna s bojovníky v Kotlin
V minulé lekci, Bojovník do arény v Kotlin, 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 souboru s metodou
main()
, ale vytvoříme si objekt Arena
, kde se bude
zápas odehrávat. V main()
si potom jen založíme objekty a o
zbytek se bude starat objekt Arena
. Přidejme k projektu tedy
poslední třídu a to Arena.kt
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 val bojovnik1: Bojovnik, private val bojovnik2: Bojovnik, val 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 fun vykresli() { println("-------------- Aréna --------------\n") println("Zdraví bojovníků: \n") println("$bojovnik1 ${bojovnik1.grafickyZivot()}") println("$bojovnik2 ${bojovnik2.grafickyZivot()}") }
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 fun vypisZpravu(zprava: String) { println(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 kurzu.
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:
fun zapas() { println("Vítejte v aréně!") println("Dnes se utkají $bojovnik1 s $bojovnik2 \n") println("Zápas může začít...") // 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ě println() } }
Kód vypíše jednoduché informace a poté 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 Main.kt
, vytvořme patřičné instance a
zavolejme na aréně metodu zapas()
:
{KOTLIN_MAIN_BLOCK} // vytvoření objektů val kostka = Kostka(10) val zalgoren = Bojovnik("Zalgoren", 100, 20, 10, kostka) val shadow = Bojovnik("Shadow", 60, 18, 15, kostka) val arena = Arena(zalgoren, shadow, kostka) // zápas arena.zapas() {/KOTLIN_MAIN_BLOCK}
{KOTLIN_OOP} class Kostka(pocetSten: Int) { val pocetSten: Int constructor() : this(6) init { this.pocetSten = pocetSten } fun hod(): Int { return (1..pocetSten).shuffled().first() } override fun toString(): String { return "Kostka s $pocetSten stěnami" } } {/KOTLIN_OOP}
{KOTLIN_OOP} import kotlin.math.* class Bojovnik(private val jmeno: String, private var zivot: Int, private val utok: Int, private val obrana: Int, private val kostka: Kostka) { private val maxZivot = zivot private var zprava = "" private fun nastavZpravu(zprava: String) { this.zprava = zprava } fun vratPosledniZpravu(): String { return zprava } fun nazivu(): Boolean { return (zivot > 0) } fun grafickyZivot(): String { var s = "[" val celkem = 20 var pocet = round((zivot.toDouble()/maxZivot) * celkem).toInt() if ((pocet == 0) && (nazivu())) pocet = 1 s = s.padEnd(pocet + s.length, '#') s = s.padEnd(celkem - pocet + s.length, ' ') s += "]" return s } fun utoc(souper: Bojovnik) { val uder = utok + kostka.hod() nastavZpravu("$jmeno útočí s úderem za $uder hp") souper.branSe(uder) } fun branSe(uder: Int) { val zraneni = uder - (obrana + kostka.hod()) if (zraneni > 0) { zivot -= zraneni zprava = "$jmeno utrpěl poškození $zraneni hp" if (zivot <= 0) { zivot = 0 zprava += " a zemřel" } } else nastavZpravu("$jmeno odrazil útok") nastavZpravu(zprava) } override fun toString(): String { return jmeno } } {/KOTLIN_OOP}
{KOTLIN_OOP} class Arena(private val bojovnik1: Bojovnik, private val bojovnik2: Bojovnik, val kostka: Kostka) { private fun vykresli() { println("-------------- Aréna --------------\n") println("Zdraví bojovníků: \n") println("$bojovnik1 ${bojovnik1.grafickyZivot()}") println("$bojovnik2 ${bojovnik2.grafickyZivot()}") } private fun vypisZpravu(zprava: String) { println(zprava) Thread.sleep(500) } fun zapas() { println("Vítejte v aréně!") println("Dnes se utkají $bojovnik1 s $bojovnik2! \n") println("Zápas může začít...") // 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ě println() } } } {/KOTLIN_OOP}
Charakteristiky hrdinů si můžete upravit dle libosti. Program spustíme:
-------------- 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 výstup 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 Kotlinu 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
bojovnik2
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:
{KOTLIN_OOP} class Arena(private val bojovnik1: Bojovnik, private val bojovnik2: Bojovnik, val kostka: Kostka) { private fun vykresli() { println("-------------- Aréna --------------\n") println("Zdraví bojovníků: \n") println("$bojovnik1 ${bojovnik1.grafickyZivot()}") println("$bojovnik2 ${bojovnik2.grafickyZivot()}") } private fun vypisZpravu(zprava: String) { println(zprava) Thread.sleep(500) } fun zapas() { // původní pořadí var b1 = bojovnik1 var b2 = bojovnik2 println("Vítejte v aréně!") println("Dnes se utkají $bojovnik1 s $bojovnik2! \n") // prohození bojovníků val zacinaBojovnik2 = kostka.hod() <= kostka.pocetSten / 2 if (zacinaBojovnik2) { b1 = bojovnik2 b2 = bojovnik1 } println("Začínat bude bojovník $b1! \n\nZápas může začít...") // 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ě } System.out.println() } } } {/KOTLIN_OOP}
{KOTLIN_MAIN_BLOCK} // vytvoření objektů val kostka = Kostka(10) val zalgoren = Bojovnik("Zalgoren", 100, 20, 10, kostka) val shadow = Bojovnik("Shadow", 60, 18, 15, kostka) val arena = Arena(zalgoren, shadow, kostka) // zápas arena.zapas() {/KOTLIN_MAIN_BLOCK}
{KOTLIN_OOP} class Kostka(pocetSten: Int) { val pocetSten: Int constructor() : this(6) init { this.pocetSten = pocetSten } fun hod(): Int { return (1..pocetSten).shuffled().first() } override fun toString(): String { return "Kostka s $pocetSten stěnami" } } {/KOTLIN_OOP}
{KOTLIN_OOP} import kotlin.math.* class Bojovnik(private val jmeno: String, private var zivot: Int, private val utok: Int, private val obrana: Int, private val kostka: Kostka) { private val maxZivot = zivot private var zprava = "" private fun nastavZpravu(zprava: String) { this.zprava = zprava } fun vratPosledniZpravu(): String { return zprava } fun nazivu(): Boolean { return (zivot > 0) } fun grafickyZivot(): String { var s = "[" val celkem = 20 var pocet = round((zivot.toDouble()/maxZivot) * celkem).toInt() if ((pocet == 0) && (nazivu())) pocet = 1 s = s.padEnd(pocet + s.length, '#') s = s.padEnd(celkem - pocet + s.length, ' ') s += "]" return s } fun utoc(souper: Bojovnik) { val uder = utok + kostka.hod() nastavZpravu("$jmeno útočí s úderem za $uder hp") souper.branSe(uder) } fun branSe(uder: Int) { val zraneni = uder - (obrana + kostka.hod()) if (zraneni > 0) { zivot -= zraneni zprava = "$jmeno utrpěl poškození $zraneni hp" if (zivot <= 0) { zivot = 0 zprava += " a zemřel" } } else nastavZpravu("$jmeno odrazil útok") nastavZpravu(zprava) } override fun toString(): String { return jmeno } } {/KOTLIN_OOP}
Program vyzkoušejme.
-------------- 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
V příští lekci, Dědičnost a polymorfismus v Kotlin, 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ě.
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 19x (40.03 kB)
Aplikace je včetně zdrojových kódů v jazyce Kotlin