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

5. diel - Bojovník do arény v Kotlin

V predchádzajúcom cvičení, Riešené úlohy k 4. lekcii OOP v Kotlin, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.

V minulej lekcii, Riešené úlohy k 4. lekcii OOP v Kotlin , sme si vysvetlili referenčné dátové typy. Už teda vieme, ako fungujú referencie a ako môžeme s objektmi zaobchádzať. Bude sa nám to hodiť dnes aj nabudúce. Tento a budúci tutoriál budú totiž venované dokončenie našej arény. Hracie kocku už máme, ešte nám chýba ďalšie 2 objekty: bojovník a samotná aréna. Dnes sa budeme venovať bojovníkovi. Najprv si popíšme, čo má bojovník vedieť, potom sa pustíme do písania kódu.

Atribúty

  • Bojovník sa bude nejako menovať.
  • Bude mať určitý počet hp (teda života, napr. 80hp).
  • Budeme uchovávať jeho maximálnej život (bude sa líšiť u každej inštancie).
  • A jeho súčasný život, teda napr. Zranený bojovník bude mať 40HP z 80tich.
  • Bojovník má určitý útok.
  • A obranu, oboje vyjadrené opäť v hp. Keď bojovník útočí s útokom 20HP na druhého bojovníka s obranou 10hp, uberie mu 10hp života.
  • Bojovník bude mať referenciu na inštanciu triedy Kostka. Pri útoku či obrane si vždy hodí kockou a k útoku / obrane pripočíta padlých číslo. (Samozrejme by mohol mať každý bojovník svoju kocku, ale chcel som sa priblížiť stolové podobe hry a ukázať, ako OOP naozaj simuluje realitu. Bojovníci teda budú zdieľať jednu inštanciu kocky.) Kockou dodáme hre prvok náhody, v realite sa jedná vlastne o šťastie, ako sa útok alebo obrana vydarí.
  • Konečne budeme chcieť, aby bojovníci podávali správy o tom, čo sa deje, pretože inak by z toho užívateľ nič nemal. Správa bude vyzerať napr. "Zalgoren útočí s úderom za 25HP.". Správami sa zatiaľ nebudeme zaťažovať a vrátime sa k nim až nakoniec.

Už vieme, čo budeme robiť, poďme na to! :) K projektu TahovyBoj si pridajme triedu Bojovnik a dodajme ju patričné atribúty. Poďme zároveň pre atribúty vytvoriť konštruktor, nebude to nič ťažké. Komentáre tu vynechám, vy si ich dopíšte podobne ako vždy. Nebudem ich písať ani u ďalších metód, aby sa tutoriál zbytočne neroztahoval a zostal prehľadný. Všetky atribúty budú privátne:

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

}

Trieda Kostka musí samozrejme byť v našom projekte.

Všimnite si, že maximálna zdravie si odvodíme a nemáme na neho parameter v hlavičke triedy. Predpokladáme, že bojovník je pri vytvorení plne zdravý, stačí nám teda poznať iba jeho život a maximálny život bude rovnaký.

Metódy

Prejdime k metódam, opäť sa najprv zamyslime nad tým, čo by mal bojovník vedieť.

  • Začnime tým jednoduchším, budeme chcieť nejakú textovú reprezentáciu, aby sme mohli bojovníka vypísať. Prekryjeme teda metódu toString(), ktorá vráti meno bojovníka.
  • Určite sa nám bude hodiť metóda, vracajúci či je bojovník nažive (teda typu Boolean). Aby to bolo trochu zaujímavejšie, budeme chcieť kresliť život bojovníka do konzoly, nebudeme teda písať, koľko má života, ale "vykreslíme" ho takto:
[#########    ]

Vyššie uvedený život by zodpovedal asi 70%. Doteraz spomínané metódy nepotrebovali žiadne parametre. Samotný útok a obranu nechajme na neskôr a poďme si implementovať toString(), nazivu() a grafickyZivot(). Začnime s toString(), tam nie je čo vymýšľať:

override fun toString(): String {
    return jmeno
}

Teraz Implementujte metódu nazivu(), opäť to nebude nič ťažké. Stačí skontrolovať, či je život väčšia ako 0 a podľa toho sa zachovať. Mohli by sme ju napísať napríklad takto:

fun nazivu(): Boolean {
    if (zivot > 0)
        return true
    else
        return false
}

Keďže aj samotný výraz (zivot > 0) je vlastne logická hodnota, môžeme vrátiť tú a kód sa značne zjednoduší:

fun nazivu(): Boolean {
    return (zivot > 0)
}

Grafický život

Ako som sa už zmienil, metóda grafickyZivot() bude umožňovať vykresliť ukazovateľ života v grafickej podobe. Už vieme, že z hľadiska objektového návrhu nie je vhodné, aby metóda objektu priamo vypisovala do konzoly (ak nie je k výpisu objekt určený). Preto si znaky uložíme do reťazca a ten vrátime pre neskoršie vypísanie. Ukážme si kód metódy a následne si ho podrobne 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
}

Pripravíme si reťazec s a vložíme doň úvodný znak "[". Určíme si celkovú dĺžku ukazovateľa života do premennej celkem (napr. 20). Teraz v podstate nepotrebujeme nič iné, než trojčlenka. Ak maxZivot zodpovedá celkem dielikov, zivot bude zodpovedať pocet dielkam. pocet je premenná s počtom dielikov aktuálneho zdravie.

Matematicky platí, že pocet = (zivot / maxZivot) * celkem. My ešte doplníme zaokrúhlení na celé dieliky a tiež pretypovanie jedného z operandov na Double, aby Kotlin chápal delenie ako desatinné.

Mali by sme ošetriť prípad, kedy je život taký nízky, že nám vyjde na 0 dielikov, ale bojovník je stále nažive. V tom prípade vykreslíme 1 dielik, inak by to vyzeralo, že je už mŕtvy.

Ďalej stačí použiť jednoduchú metódu padEnd(), ktorá pridá na koniec String u určitý znak toľkokrát, aby mal vo výsledku String špecifikovanú dĺžku.

Všetko si vyskúšame, prejdime k metóde main() a vytvorme si bojovníka (a kocku, pretože tu musíme konstruktoru bojovníka odovzdať). Následne výpisy, či je nažive 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ávame sa k samotnému boju. Implementujeme metódy pre útok a obranu.

Obrana

Začnime obranou. Metóda branSe() bude umožňovať brániť sa úderu, ktorého sila bude odovzdaná metóde ako parameter. Metódu si opäť ukážeme a potom popíšeme:

fun branSe(uder: Int) {
    val zraneni = uder - (obrana + kostka.hod())
    if (zraneni > 0) {
        zivot -= zraneni
        if (zivot <= 0)
            zivot = 0
    }
}

Najprv spočítame skutočné zranenia a to tak, že z útoku nepriateľa odpočítame našu obranu zvýšenú o číslo, ktoré padlo na hracej kocke. Ak sme zranenia celej neodrazil (zraneni > 0), budeme znižovať náš život. Táto podmienka je dôležitá, keby sme zranenia odrazili a bolo napr. -2, bez podmienky by sa život zvýšil. Po znížení života skontrolujeme, či nie je v zápornej hodnote a prípadne ho dorovnáme na nulu.

Útok

Metóda utoc() bude brať ako parameter inštanciu bojovníka, na ktorého sa útočí. To preto, aby sme na ňom mohli zavolať metódu branSe(), ktorá na náš útok zareaguje a zmenší protivníkov život. Tu vidíme výhody referencií v Kotlinu, môžeme si inštancie jednoducho odovzdávať a volať na nich metódy, bez toho aby došlo k ich skopírovanie. Ako prvý vypočítame úder, podobne ako pri obrane, úder bude náš útok + hodnota z hracej kocky. Na súperovi následne zavoláme metódu branSe() s hodnotou úderu:

fun utoc(souper: Bojovnik) {
    val uder = utok + kostka.hod()
    souper.branSe(uder)
}

To by sme mali, poďme si skúsiť v našom ukážkovom programe zaútočiť a potom znova vykresliť život. Pre jednoduchosť nemusíme zakladať ďalšieho bojovníka, ale môžeme zaútočiť sami na seba:

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á sa, že všetko funguje, ako má. Prejdime k poslednému bodu dnešného tutoriálu a to k správam.

Správy

Ako už bolo povedané, o útokoch a obrane budeme užívateľa informovať výpisom na konzolu. Výpis nebude vykonávať samotná trieda Bojovnik, tá bude len vracať správy ako textové reťazce. Jedna možnosť by bola nastaviť návratový typ metód utoc() a branSe() na String a pri ich volanie vrátiť aj správu. Problém by však nastal v prípade, keď by sme chceli získať správu od metódy, ktorá už niečo vracia. Metóda samozrejme nemôže jednoducho vrátiť 2 veci.

Poďme na vec univerzálnejšie, správu budeme ukladať do privátnej premennej zprava a urobíme metódy pre jej uloženie a vrátenie. Samozrejme by sme mohli urobiť premennú verejnú, ale nie je tu dôvod, prečo umožniť zvonku zápis do správy a tiež by skladanie zložitejšie správy vnútri triedy mohlo byť niekedy problematické.

K atribútom triedy teda pridáme:

private var zprava = ""

Teraz si vytvoríme dve metódy. Privátne nastavZpravu(), ktorá berie ako parameter text správy a slúži na interné účely triedy, kde nastaví správu do privátnej premennej:

private fun nastavZpravu(zprava: String) {
    this.zprava = zprava
}

Nič zložité. Podobne jednoduchá bude verejná metóda pre navrátenie správy:

fun vratPosledniZpravu(): String {
    return zprava
}

O prácu so správami obohatíme naše metódy utoc() a branSe(), teraz budú vyzerať 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šetko si opäť vyskúšame, tentoraz už vytvorí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 kocku i bojovníka, teraz už chýba len aréna. Tú si vytvoríme hneď v nasledujúcej lekcii, Aréna s bojovníkmi v Kotlin .


 

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 472x (30.4 kB)

 

Predchádzajúci článok
Riešené úlohy k 4. lekcii OOP v Kotlin
Všetky články v sekcii
Objektovo orientované programovanie v Kotlin
Preskočiť článok
(neodporúčame)
Aréna s bojovníkmi v Kotlin
Článok pre vás napísal Samuel Kodytek
Avatar
Užívateľské hodnotenie:
1 hlasov
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