Lekce 5 - Bojovník do arény v Kotlin
V předešlém cvičení, Řešené úlohy k 4. lekci OOP v Kotlin, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
Tento tutoriál je věnován 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.
- 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 třídy
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! K projektu TahovyBoj
si přidejme třídu Bojovnik
a dodejme ji patřičné atributy.
Pojďme zároveň pro atributy vytvořit konstruktor, nebude to nic těžkého.
Komentáře zde vynechám, vy si je dopište podobně jako vždy. Nebudu je
psát ani u dalších metod, aby se tutoriál zbytečně neroztahoval a zůstal
přehledný. Všechny atributy budou privátní:
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 }
Třída Kostka
musí samozřejmě být v našem projektu.
Všimněte si, že maximální zdraví si odvodíme a nemáme na něj parametr v hlavičce třídy. Předpokládáme, že bojovník je při vytvoření plně zdravý, stačí nám tedy znát pouze jeho život a maximální život bude stejný.
Metody
Přejděme k metodám, 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
toString()
, která vrátí jméno bojovníka. - Určitě se nám bude hodit metoda, vracející zda je bojovník naživu
(tedy typu
Boolean
). 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 toString()
, nazivu()
a
grafickyZivot()
. Začněme s toString()
, tam není co
vymýšlet:
override fun toString(): String { return jmeno }
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. Mohli bychom ji napsat třeba takto:
fun nazivu(): Boolean { if (zivot > 0) return true else return false }
Jelikož i samotný výraz (zivot > 0)
je vlastně logická
hodnota, můžeme vrátit tu a kód se značně zjednoduší:
fun nazivu(): Boolean { return (zivot > 0) }
Grafický život
Jak jsem se již zmínil, metoda grafickyZivot()
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í. Ukažme si
kód metody a následně si jej podrobně popíšeme:
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 }
Připravíme si řetězec s
a vložíme do něj úvodní znak
"["
. 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 maxZivot
odpovídá
celkem
dílků, zivot
bude odpovídat
pocet
dílkům. pocet
je proměnná s počtem dílků
aktuálního zdraví.
Matematicky platí, že
pocet = (zivot / maxZivot) * celkem
. My ještě
doplníme zaokrouhlení na celé dílky a také přetypování jednoho z
operandů na Double
, aby Kotlin chápal dělení jako
desetinné.
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 stačí použít jednoduchou metodu padEnd()
, která
přidá na konec String
u určitý znak tolikrát, aby měl ve
výsledku String
specifikovanou délku.
Vše si vyzkoušíme, přejděme k metodě main()
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:
{KOTLIN_MAIN_BLOCK} val kostka = Kostka(10) val bojovnik = Bojovnik("Zalgoren", 100, 20, 10, kostka) println("Bojovník: $bojovnik") // test toString() println("Naživu: ${bojovnik.nazivu()}") // test nazivu() println("Život: ${bojovnik.grafickyZivot()}") // test grafickyZivot() {/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 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 } override fun toString(): String { return jmeno } } {/KOTLIN_OOP}
Výstup:
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. Metoda branSe()
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:
fun branSe(uder: Int) { val zraneni = uder - (obrana + kostka.hod()) if (zraneni > 0) { zivot -= zraneni if (zivot <= 0) 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
branSe()
, která na náš útok zareaguje a zmenší protivníkův
život. Zde vidíme výhody referencí v Kotlinu, 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 branSe()
s hodnotou úderu:
fun utoc(souper: Bojovnik) { val uder = utok + kostka.hod() souper.branSe(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:
{KOTLIN_MAIN_BLOCK} val kostka = Kostka(10) val bojovnik = Bojovnik("Zalgoren", 100, 20, 10, kostka) println("Bojovník: $bojovnik") // test toString() println("Naživu: ${bojovnik.nazivu()}") // test nazivu() println("Život: ${bojovnik.grafickyZivot()}") // test grafickyZivot() bojovnik.utoc(bojovnik) // test útoku println("Život po útoku: ${bojovnik.grafickyZivot()}") {/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 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 branSe(uder: Int) { val zraneni = uder - (obrana + kostka.hod()) if (zraneni > 0) { zivot -= zraneni if (zivot <= 0) zivot = 0 } } fun utoc(souper: Bojovnik) { val uder = utok + kostka.hod() souper.branSe(uder) } override fun toString(): String { return jmeno } } {/KOTLIN_OOP}
Výstup:
Bojovník: Zalgoren Naživu: true Život: [####################] Život po útoku: [################## ]
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 nastavit návratový typ metod utoc()
a
branSe()
na String
a při jejich volání 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 2 věci.
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é.
K atributům třídy tedy přidáme:
private var zprava = ""
Nyní si vytvoříme dvě metody. Privátní nastavZpravu()
,
která bere jako parametr text zprávy a slouží pro vnitřní účely třídy,
kde nastaví zprávu do privátní proměnné:
private fun nastavZpravu(zprava: String) { this.zprava = zprava }
Nic složitého. Podobně jednoduchá bude veřejná metoda pro navrácení zprávy:
fun vratPosledniZpravu(): String { return zprava }
O práci se zprávami obohatíme naše metody utoc()
a
branSe()
, nyní budou vypadat takto:
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) }
Vše si opět vyzkoušíme, tentokrát již vytvoříme druhého bojovníka:
{KOTLIN_MAIN_BLOCK} val kostka = Kostka(10) val bojovnik = Bojovnik("Zalgoren", 100, 20, 10, kostka) println("Život: ${bojovnik.grafickyZivot()}") // // test GrafickyZivot() // útok na našeho bojovníka val souper = Bojovnik("Shadow", 60, 18, 15, kostka) souper.utoc(bojovnik) println(souper.vratPosledniZpravu()) println(bojovnik.vratPosledniZpravu()) println("Život po útoku: ${bojovnik.grafickyZivot()}") {/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}
Výstup:
Život: [####################] Shadow útočí s úderem za 24 hp Zalgoren utrpěl poškození 10 hp Život po útoku: [################## ]
Máme kostku i bojovníka.
V příští lekci, Aréna s bojovníky v Kotlin, se budeme věnovat tvorbě arény, která je poslední chybějící částí aplikace.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkamiStaženo 519x (30.4 kB)