Vydělávej až 160.000 Kč měsíčně! Akreditované rekvalifikační kurzy s garancí práce od 0 Kč. Více informací.
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 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 Stringu 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:

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

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:

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

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

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

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:

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

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ínkami

Staženo 472x (30.4 kB)

 

Předchozí článek
Řešené úlohy k 4. lekci OOP v Kotlin
Všechny články v sekci
Objektově orientované programování v Kotlin
Přeskočit článek
(nedoporučujeme)
Aréna s bojovníky v Kotlin
Článek pro vás napsal Samuel Kodytek
Avatar
Uživatelské hodnocení:
14 hlasů
Autor se věnuje všem jazykům okolo JVM. Rád pomáhá lidem, kteří se zajímají o programování. Věří, že všichni mají šanci se naučit programovat, jen je potřeba prorazit tu bariéru, který se říká lenost.
Aktivity