NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
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 6 - Aréna s bojovníky ve Swift

V minulé lekci, Bojovník do arény ve Swift, 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 ve Swift 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 main.swift, ale vytvoříme si objekt Arena, kde se bude zápas odehrávat. Soubor main.swift potom jen založí objekty a o zbytek se bude starat objekt Arena. Přidejme k projektu tedy poslední třídu a to Arena.swift.

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

class Arena {
    private var bojovnik1 : Bojovnik
    private var bojovnik2 : Bojovnik
    private var kostka : Kostka

    init(bojovnik1: Bojovnik, bojovnik2: Bojovnik, kostka: Kostka) {
        self.bojovnik1 = bojovnik1
        self.bojovnik2 = bojovnik2
        self.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:

func vykresli() {
    print("\n \n \n \n \n \n \n \n")
    print("-------------- Aréna -------------- \n")
    print("Zdraví bojovníků: \n")
    print("\(bojovnik1) \(bojovnik1.grafickyZivot())")
    print("\(bojovnik2) \(bojovnik2.grafickyZivot())")
}

Zde asi není co řešit. Jelikož nejde rozumně smazat výstup, tak si vše dostatečně odřádkujeme, abychom viděli vždy pouze aktuální výstup. 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 func vypisZpravu(_ zprava: String) {
    print(zprava)
    sleep(1)
}

Kód je zřejmý až na metodu sleep(), která uspí vlákno programu na daný počet sekund.

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:

func zapas() {
    print("Vítejte v aréně!")
    print("Dnes se utkají \(bojovnik1) s \(bojovnik2)! \n")
    print("Zápas může začít...")
    readLine()
    // cyklus s bojem
    while bojovnik1.nazivu() && bojovnik2.nazivu() {
        bojovnik1.utoc(souper: bojovnik2)
        vykresli()
        vypisZpravu(bojovnik1.vratPosledniZpravu()) // zpráva o útoku
        vypisZpravu(bojovnik2.vratPosledniZpravu()) // zpráva o obraně
        bojovnik2.utoc(souper: bojovnik1)
        vykresli()
        vypisZpravu(bojovnik2.vratPosledniZpravu()) // zpráva o útoku
        vypisZpravu(bojovnik1.vratPosledniZpravu()) // zpráva o obraně
        print(" ")
    }
}

Kód vypíše jednoduché informace a po stisku enteru (abychom potvrdili vstup pro readLine()) 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.swift, vytvořme patřičné instance a zavolejme na aréně metodu zapas():

Klikni pro editaci
  • App
    • main.swift
    • Kostka.swift
    • Bojovnik.swift
    • Arena.swift
  • // vytvoření objektů
    let kostka = Kostka(pocetSten: 10)
    let zalgoren = Bojovnik(jmeno: "Zalgoren", zivot: 100, utok: 20, obrana: 10, kostka: kostka)
    let shadow = Bojovnik(jmeno: "Shadow", zivot: 60, utok: 18, obrana: 15, kostka: kostka)
    let arena = Arena(bojovnik1: zalgoren, bojovnik2: shadow, kostka: kostka)
    // zápas
    arena.zapas()
    
  • class Kostka : CustomStringConvertible {
    
        var description: String {
            return "Kostka s \(pocetSten) stěnami"
        }
    
        private var pocetSten : Int
    
        init() {
            pocetSten = 6
        }
    
        init(pocetSten: Int) {
            self.pocetSten = pocetSten
        }
    
        func vratPocetSten() -> Int {
            return pocetSten
        }
    
        func hod() -> Int {
            return Int(arc4random_uniform(UInt32(pocetSten))) + 1
        }
    }
    
  • class Bojovnik: CustomStringConvertible {
    
        private var jmeno : String
    
        private var zivot : Double
    
        private var maxZivot : Double
    
        private var utok : Int
    
        private var obrana : Int
    
        private var kostka : Kostka
    
        private var zprava : String = ""
    
        init(jmeno: String, zivot: Int, utok: Int, obrana: Int, kostka: Kostka) {
            self.jmeno = jmeno
            self.zivot = Double(zivot)
            self.maxZivot = self.zivot
            self.utok = utok
            self.obrana = obrana
            self.kostka = kostka
        }
    
        var description: String {
            return jmeno
        }
    
        func nazivu() -> Bool {
            return zivot > 0
        }
    
        func grafickyZivot() -> String {
            var s = "["
            let celkem : Double = 20
            var pocet : Double = round((zivot / maxZivot) * celkem)
            if (pocet == 0) && (nazivu()) {
                pocet = 1
            }
            for _ in 0..<Int(pocet) {
                s += "#"
            }
            s = s.padding(toLength: Int(celkem) + 1, withPad: " ", startingAt: 0)
            s += "]"
            return s
        }
    
        func utoc(souper: Bojovnik) {
            let uder = utok + kostka.hod()
            nastavZpravu("\(jmeno) útočí s úderem za \(uder) hp")
            souper.branSe(uder: uder)
        }
    
        func branSe(uder: Int) {
            let zraneni = Double(uder - (obrana + kostka.hod()))
            var zprava = ""
            if (zraneni > 0) {
                zivot -= zraneni
                zprava = "\(jmeno) utrpěl poškození \(Int(zraneni)) hp"
                if (zivot <= 0) {
                    zivot = 0
                }
            } else {
                zprava = "\(jmeno) odrazil útok"
            }
            nastavZpravu(zprava)
        }
    
        private func nastavZpravu(_ zprava: String) {
            self.zprava = zprava
        }
    
        func vratPosledniZpravu() -> String {
            return zprava
        }
    }
    
  • class Arena {
        private var bojovnik1 : Bojovnik
        private var bojovnik2 : Bojovnik
        private var kostka : Kostka
    
        init(bojovnik1: Bojovnik, bojovnik2: Bojovnik, kostka: Kostka) {
            self.bojovnik1 = bojovnik1
            self.bojovnik2 = bojovnik2
            self.kostka = kostka
        }
    
        func vykresli() {
            print("\n \n \n \n \n \n \n \n")
            print("-------------- Aréna -------------- \n")
            print("Zdraví bojovníků: \n")
            print("\(bojovnik1) \(bojovnik1.grafickyZivot())")
            print("\(bojovnik2) \(bojovnik2.grafickyZivot())")
        }
    
        private func vypisZpravu(_ zprava: String) {
            print(zprava)
            sleep(1)
        }
    
        func zapas() {
            print("Vítejte v aréně!")
            print("Dnes se utkají \(bojovnik1) s \(bojovnik2)! \n")
            print("Zápas může začít...")
            readLine()
            // cyklus s bojem
            while bojovnik1.nazivu() && bojovnik2.nazivu() {
                bojovnik1.utoc(souper: bojovnik2)
                vykresli()
                vypisZpravu(bojovnik1.vratPosledniZpravu()) // zpráva o útoku
                vypisZpravu(bojovnik2.vratPosledniZpravu()) // zpráva o obraně
                bojovnik2.utoc(souper: bojovnik1)
                vykresli()
                vypisZpravu(bojovnik2.vratPosledniZpravu()) // zpráva o útoku
                vypisZpravu(bojovnik1.vratPosledniZpravu()) // zpráva o obraně
                print(" ")
            }
        }
    }
    
    
    • Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.

    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ýpis 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 5, 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 ve Swift 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.

    Ještě se zbavíme varování u readLine(). Metoda totiž vrací String? (Optional), ale nás vrácená hodnota nezajímá. Proto ji přiřadíme do "_", čímž se právě tato situace ve Swift označuje. Výhodné to je i pro nás či dalšího programátora, protože hned víme, že tuto hodnotu dále nepotřebujeme a kód je tak čitelnější.

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

    Klikni pro editaci
    • App
      • Arena.swift
      • main.swift
      • Kostka.swift
      • Bojovnik.swift
    •     func zapas() {
              // původní pořadí
              var b1 = bojovnik1
              var b2 = bojovnik2
              print("Vítejte v aréně!")
              print("Dnes se utkají \(bojovnik1) s \(bojovnik2)! \n")
              // prohození bojovníků
              let zacinaBojovnik2 = kostka.hod() <= kostka.vratPocetSten() / 2
              if (zacinaBojovnik2) {
                  b1 = bojovnik2
                  b2 = bojovnik1
              }
              print("Začínat bude bojovník \(b1)! \nZápas může začít...")
              _ = readLine()
              // cyklus s bojem
              while b1.nazivu() && b2.nazivu() {
                  b1.utoc(souper: b2)
                  vykresli()
                  vypisZpravu(b1.vratPosledniZpravu()) // zpráva o útoku
                  vypisZpravu(b2.vratPosledniZpravu()) // zpráva o obraně
                  if (b2.nazivu()) {
                      b2.utoc(souper: b1)
                      vykresli()
                      vypisZpravu(b2.vratPosledniZpravu()) // zpráva o útoku
                      vypisZpravu(b1.vratPosledniZpravu()) // zpráva o obraně
                  }
                  print(" ")
              }
          }
      
    • // vytvoření objektů
      let kostka = Kostka(pocetSten: 10)
      let zalgoren = Bojovnik(jmeno: "Zalgoren", zivot: 100, utok: 20, obrana: 10, kostka: kostka)
      let shadow = Bojovnik(jmeno: "Shadow", zivot: 60, utok: 18, obrana: 15, kostka: kostka)
      let arena = Arena(bojovnik1: zalgoren, bojovnik2: shadow, kostka: kostka)
      // zápas
      arena.zapas()
      
    • class Kostka : CustomStringConvertible {
      
          var description: String {
              return "Kostka s \(pocetSten) stěnami"
          }
      
          private var pocetSten : Int
      
          init() {
              pocetSten = 6
          }
      
          init(pocetSten: Int) {
              self.pocetSten = pocetSten
          }
      
          func vratPocetSten() -> Int {
              return pocetSten
          }
      
          func hod() -> Int {
              return Int(arc4random_uniform(UInt32(pocetSten))) + 1
          }
      }
      
    • class Bojovnik: CustomStringConvertible {
      
          private var jmeno : String
      
          private var zivot : Double
      
          private var maxZivot : Double
      
          private var utok : Int
      
          private var obrana : Int
      
          private var kostka : Kostka
      
          private var zprava : String = ""
      
          init(jmeno: String, zivot: Int, utok: Int, obrana: Int, kostka: Kostka) {
              self.jmeno = jmeno
              self.zivot = Double(zivot)
              self.maxZivot = self.zivot
              self.utok = utok
              self.obrana = obrana
              self.kostka = kostka
          }
      
          var description: String {
              return jmeno
          }
      
          func nazivu() -> Bool {
              return zivot > 0
          }
      
          func grafickyZivot() -> String {
              var s = "["
              let celkem : Double = 20
              var pocet : Double = round((zivot / maxZivot) * celkem)
              if (pocet == 0) && (nazivu()) {
                  pocet = 1
              }
              for _ in 0..<Int(pocet) {
                  s += "#"
              }
              s = s.padding(toLength: Int(celkem) + 1, withPad: " ", startingAt: 0)
              s += "]"
              return s
          }
      
          func utoc(souper: Bojovnik) {
              let uder = utok + kostka.hod()
              nastavZpravu("\(jmeno) útočí s úderem za \(uder) hp")
              souper.branSe(uder: uder)
          }
      
          func branSe(uder: Int) {
              let zraneni = Double(uder - (obrana + kostka.hod()))
              var zprava = ""
              if (zraneni > 0) {
                  zivot -= zraneni
                  zprava = "\(jmeno) utrpěl poškození \(Int(zraneni)) hp"
                  if (zivot <= 0) {
                      zivot = 0
                  }
              } else {
                  zprava = "\(jmeno) odrazil útok"
              }
              nastavZpravu(zprava)
          }
      
          private func nastavZpravu(_ zprava: String) {
              self.zprava = zprava
          }
      
          func vratPosledniZpravu() -> String {
              return zprava
          }
      }
      
      • Zkontroluj, zda výstupy programu odpovídají předloze. S jinými texty testy neprojdou.

      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 ve Swift, 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 9x (25.59 kB)
      Aplikace je včetně zdrojových kódů v jazyce Swift

       

      Jak se ti líbí článek?
      Před uložením hodnocení, popiš prosím autorovi, co je špatněZnaků 0 z 50-500
      Předchozí článek
      Bojovník do arény ve Swift
      Všechny články v sekci
      Objektově orientované programování ve Swift
      Přeskočit článek
      (nedoporučujeme)
      Dědičnost a polymorfismus ve Swift
      Článek pro vás napsal Filip Němeček
      Avatar
      Uživatelské hodnocení:
      5 hlasů
      Autor se věnuje vývoji iOS aplikací (občas macOS)
      Aktivity