8. diel - Bojovník do arény v Pythone
V predchádzajúcom cvičení, Riešené úlohy k 6.-7. lekciu OOP v Pythone, sme si precvičili získané skúsenosti z predchádzajúcich lekcií.
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 Python tutoriál budú venované dokončeniu našej arény. Hraciu kocku už máme, ešte nám chýbajú ďalšie dva 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 volať a bude mať určitý počet
hp (teda bodov života, napr. 80hp). Budeme uchovávať jeho
maximálny život (bude sa líšiť pri každej inštancii) a
jeho súčasný život, teda napr. zranený bojovník bude mať
40hp z 80. 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štancii objektu
RollingDie
. Pri útoku či obrane si vždy hodí kockou a
k útoku/obrane pripočíta padnuté číslo. (Samozrejme by mohol mať každý
bojovník svoju kocku, ale chcel som sa priblížiť stolovej 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í. Napokon 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! 🙂 Najprv upravíme štruktúru
nášho projektu ArenaFight
. Triedu RollingDie
v
súbore rolling_die.py
máme hotovú a ďalej sa jej nebudeme
zaoberať, len ju budeme používať. Aby to bolo možné, budeme si ju musieť
do všetkých ďalších súborov, kde ju chceme využiť, najskôr importovať.
V našom prípade to bude import do nového súboru main.py
, ktorý
si založíme. V ňom bude hlavný kód programu, teda to, čo budeme písať
mimo triedy. Arénu budeme mať v súbore
arena.py
. Ten si len založíme a necháme nabudúce. Štruktúru
projektu tým máme hotovú a nič nám nebráni pustiť sa do bojovníka. Do
nového súboru warrior.py
si teda pridajme triedu
Warrior
. Za moment jej dodáme aj patričné atribúty. Všetky
budú privátne. Trieda Warrior
zatiaľ vyzerá teda takto:
class Warrior: """ The class represents an arena warrior. """
Metódy
Poďme pre atribúty vytvoriť konštruktor, nebude to nič ťažké. Komentáre tu vynechám, vy si ich dopíšte podobne, ako u atribútov vyššie. Nebudem ich písať ani pri ďalších metódach, aby sa tutoriál zbytočne nerozťahoval a zostal prehľadný (prípadne sa pozrite do archívu):
def __init__(self, name, health, damage, defense, die): self._name = name self._health = health self._max_health = health self._damage = damage self._defense = defense self._die = die
Všimnime si, že maximálne zdravie si v konštruktore odvodíme a nemáme naň parameter v hlavičke. Predpokladáme, že je bojovník pri vytvorení plne zdravý. Stačí nám teda poznať iba jeho život a maximálny život bude rovnaký.
Prejdime k metódam a opäť sa najskôr 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
__str__()
, ktorá vráti meno bojovníka. Určite sa nám bude
hodiť metóda vracajúca, či je bojovník nažive. 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ť metódy __str__()
, is_alive()
a health_bar()
. Začnime sa __str__()
, tam nie je čo
vymýšľať:
def __str__(self): return str(self._name)
Teraz implementujme metódu is_alive()
, opäť to nebude nič
ťažké. Stačí skontrolovať, či je život väčší ako 0
a
podľa toho sa zachovať:
def is_alive(self): if self._health > 0: return True else: return False
Keďže aj samotný výraz (self._health > 0
) je vlastne
logická hodnota, môžeme vrátiť tú a kód sa značne zjednoduší:
def is_alive(self): return self._health > 0
Grafický život
Ako sme už spomínali, metóda health_bar()
umožní 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
(pokiaľ nie je na výpis objekt určený), preto si znaky uložíme do reťazca
a ten vrátime pre neskoršie vypísanie. Pozrime sa na kód metódy a následne
si ho podrobne popíšeme:
def health_bar(self): total = 20 count = int(self._health / self._max_health * total) if (count == 0 and self.is_alive()): count = 1 return f"[{'#'*count}{' '*(total-count)}]"
Určíme si celkovú dĺžku ukazovateľa života do premennej
total
(napríklad 20). Teraz v podstate nepotrebujeme nič iné,
než trojčlenku. Pokiaľ max_health
zodpovedá total
dielikom, health
bude zodpovedať count
dielikom.
Premenná count
je počet dielikov aktuálneho zdravia.
Matematicky platí, že count = (health / max_health) * total
.
My ešte doplníme pretypovanie na celé číslo.
Mali by sme ošetriť prípad, keď 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 jeden dielik, inak by to vyzeralo, že je už mŕtvy. Ďalej využijeme formátovanie a replikovanie.
Všetko si vyskúšame. Prejdime do súboru main.py
a importujme
si triedy Warrior
a RollingDie
:
from warrior import Warrior from rolling_die import RollingDie
Ďalej si vytvoríme inštanciu bojovníka (a kocky, pretože tu musíme odovzdať konštruktoru bojovníka). Následne vypíšeme, či je nažive a jeho život graficky:
{PYTHON} from rolling_die import RollingDie from warrior import Warrior die = RollingDie(10) warrior = Warrior("Zalgoren", 100, 20, 10, die) print(f"Warrior: {warrior}") # test __str__() print(f"Alive: {warrior.is_alive()}") # test is_alive() print(f"Health: {warrior.health_bar()}") # test health_bar() {/PYTHON}
{PYTHON} class Warrior: """ The class represents an arena warrior. """ def __init__(self, name, health, damage, defense, die): """ name - the warrior's name health - maximum health damage - damage defense - defense die - a rolling die instance """ self._name = name self._health = health self._max_health = health self._damage = damage self._defense = defense self._die = die def __str__(self): return str(self._name) def is_alive(self): return self._health > 0 def health_bar(self): total = 20 count = int(self._health / self._max_health * total) if (count == 0 and self.is_alive()): count = 1 return f"[{'#'*count}{' '*(total-count)}]" {/PYTHON}
{PYTHON} class RollingDie: """ Class representing a die for a board game. """ def __init__(self,sides_count=6): self._sides_count = sides_count def get_sides_count(self): """ Returns the number of sides the die has. """ return self._sides_count def roll(self): """ Rolls a die and returns a number from 1 to the sides count. """ import random as _random return _random.randint(1, self._sides_count) def __str__(self): """ Returns a textual representation of our die. """ return f"RollingDie({self._sides_count})" {/PYTHON}
Pozrime sa na výstup:
Zalgoren lives!
Warrior: Zalgoren
Alive: True
Health: [####################]
Boj
Dostávame sa k samotnému boju. Implementujeme metódy na útok a obranu.
Obrana
Začnime obranou. Vrátime sa do súboru warrior.py
a do triedy
Warrior
pridáme obrannú metódu. Metóda defend()
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:
def defend(self, hit): injury = hit - (self._defense + self._die.roll()) if injury > 0: self._health = self._health - injury if self._health < 0: self._health = 0
Najprv spočítame skutočné zranenie a to tak, že z útoku nepriateľa
odpočítame našu obranu zvýšenú o číslo, ktoré padlo na hracej kocke. Ak
sme zranenie celé neodrazili (injury > 0
), budeme znižovať
náš život. Táto podmienka je dôležitá, keby sme zranenia odrazili a bolo
napríklad -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 attack()
bude brať ako parameter inštanciu bojovníka,
na ktorého sa útočí. To preto, aby sme na ňom mohli zavolať metódu
defend()
, ktorá na náš útok zareaguje a zmenší protivníkov
život. Tu vidíme výhody referencií v Pythone, môžeme si inštancie
jednoducho odovzdávať a volať na nich metódy bez toho, aby došlo k ich
skopírovaniu. Ako prvý vypočítame úder. Podobne ako pri obrane bude úder
tvoriť náš útok + hodnota z hracej kocky. Na súperovi následne zavoláme
metódu defend()
s hodnotou úderu:
def attack(self, enemy):
hit = self._damage + self._die.roll()
enemy.defend(hit)
To by sme mali, poďme si skúsiť v našom hlavnom programe (v
main.py
) zaútočiť a potom znova vykresliť život. Pre
jednoduchosť nebudeme zakladať ďalšieho bojovníka, ale zaútočíme sami na
seba:
{PYTHON} from rolling_die import RollingDie from warrior import Warrior die = RollingDie(10) warrior = Warrior("Zalgoren", 100, 20, 10, die) print(f"Warrior: {warrior}") # test __str__() print(f"Alive: {warrior.is_alive()}") # test is_alive() print(f"Health: {warrior.health_bar()}") # test health_bar() warrior.attack(warrior) print(f"Health after the hit: {warrior.health_bar()}") {/PYTHON}
{PYTHON} class Warrior: """ The class represents an arena warrior. """ def __init__(self, name, health, damage, defense, die): """ name - the warrior's name health - maximum health damage - damage defense - defense die - a rolling die instance """ self._name = name self._health = health self._max_health = health self._damage = damage self._defense = defense self._die = die def __str__(self): return str(self._name) def is_alive(self): return self._health > 0 def health_bar(self): total = 20 count = int(self._health / self._max_health * total) if (count == 0 and self.is_alive()): count = 1 return f"[{'#'*count}{' '*(total-count)}]" def defend(self, hit): injury = hit - (self._defense + self._die.roll()) if injury > 0: self._health = self._health - injury if self._health < 0: self._health = 0 def attack(self, enemy): hit = self._damage + self._die.roll() enemy.defend(hit) {/PYTHON}
{PYTHON} class RollingDie: """ Class representing a die for a board game. """ def __init__(self,sides_count=6): self._sides_count = sides_count def get_sides_count(self): """ Returns the number of sides the die has. """ return self._sides_count def roll(self): """ Rolls a die and returns a number from 1 to the sides count. """ import random as _random return _random.randint(1, self._sides_count) def __str__(self): """ Returns a textual representation of our die. """ return f"RollingDie({self._sides_count})" {/PYTHON}
Všetko funguje, ako má:
Zalgoren is injured!
Warrior: Zalgoren
Alive: True
Health: [####################]
Health after the hit: [################# ]
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ľov informovať
výpisom na konzole. Výpis nebude vykonávať samotná trieda
Warrior
, tá bude len vracať správy ako textové reťazce. Jedna
možnosť by bola pri volaní metód attack()
a
defend()
vrátiť aj správu. Problém by však nastal v prípade,
ak by sme chceli získať správu od metódy, ktorá už niečo vracia. Metóda
samozrejme nemôže jednoducho vrátiť dve veci.
Poďme na vec univerzálnejšie, správu budeme ukladať do privátnej
premennej message
a napíšeme metódy na jej uloženie a
vrátenie. Samozrejme by sme mohli urobiť premennú verejnú (a fungovalo by to
rovnako), ale nie je dôvod, prečo umožniť zvonku zápis do správy.
Skladanie zložitejšie správy vo vnútri triedy by tiež mohlo byť niekedy
problematické.
Vždy je lepšie byť "prehnane" dôsledný a využívať zapuzdrenie všade, kde je to možné.
Do metódy konštruktora bojovníka __init__()
teda
pridáme:
self._message = ""
Teraz si v triede Warrior
vytvoríme dve metódy. Privátnu
metódu _set_message()
, ktorá berie ako parameter text správy a
slúži na vnútorné účely triedy, kde nastaví správu do privátnej
premennej:
def _set_message(self, message):
self._message = message
Nič zložité. Podobne jednoduchá bude verejná metóda na vrátenie správy:
def get_last_message(self): return self._message
O práci so správami obohatíme naše metódy attack()
a
defend()
. Teraz budú vyzerať takto:
def defend(self, hit): injury = hit - (self._defense + self._die.roll()) if injury > 0: message = f"{self._name} defended against the attack but still lost {injury} hp." self._health = self._health - injury if self._health <= 0: self._health = 0 message = f"{message[:-1]} and died." else: message = f"{self._name} blocked the hit." self._set_message(message) def attack(self, enemy): hit = self._damage + self._die.roll() message = f"{self._name} attacks with a hit worth {hit} hp." self._set_message(message) enemy.defend(hit)
Všetko si opäť vyskúšame v main.py
Tentokrát už
vytvoríme druhého bojovníka:
{PYTHON} from rolling_die import RollingDie from warrior import Warrior die = RollingDie(10) warrior = Warrior("Zalgoren", 100, 20, 10, die) print(f"Health: {warrior.health_bar()}") # warrior attack phase enemy = Warrior("Shadow", 60, 18, 15, die) enemy.attack(warrior) print(enemy.get_last_message()) print(warrior.get_last_message()) print(f"Health: {warrior.health_bar()}") {/PYTHON}
{PYTHON} class Warrior: """ The class represents an arena warrior. """ def __init__(self, name, health, damage, defense, die): """ name - the warrior's name health - maximum health damage - damage defense - defense die - a rolling die instance """ self._name = name self._health = health self._max_health = health self._damage = damage self._defense = defense self._die = die self._message = "" def __str__(self): return str(self._name) def is_alive(self): return self._health > 0 def health_bar(self): total = 20 count = int(self._health / self._max_health * total) if (count == 0 and self.is_alive()): count = 1 return f"[{'#'*count}{' '*(total-count)}]" def defend(self, hit): injury = hit - (self._defense + self._die.roll()) if injury > 0: message = f"{self._name} defended against the attack but still lost {injury} hp." self._health = self._health - injury if self._health <= 0: self._health = 0 message = f"{message[:-1]} and died." else: message = f"{self._name} blocked the hit." self._set_message(message) def attack(self, enemy): hit = self._damage + self._die.roll() message = f"{self._name} attacks with a hit worth {hit} hp." self._set_message(message) enemy.defend(hit) def _set_message(self, message): self._message = message def get_last_message(self): return self._message {/PYTHON}
{PYTHON} class RollingDie: """ Class representing a die for a board game. """ def __init__(self,sides_count=6): self._sides_count = sides_count def get_sides_count(self): """ Returns the number of sides the die has. """ return self._sides_count def roll(self): """ Rolls a die and returns a number from 1 to the sides count. """ import random as _random return _random.randint(1, self._sides_count) def __str__(self): """ Returns a textual representation of our die. """ return f"RollingDie({self._sides_count})" {/PYTHON}
Pozrime sa na výsledok:
Shadow attacks!
Health: [####################]
Shadow attacks with a hit worth 24 hp.
Zalgoren defended against the attack but still lost 7 hp.
Health: [################## ]
To je k dnešnej lekcii všetko
V ďalšej lekcii, Aréna s bojovníkmi v Pythone, dokončíme arénu simulujúcu zápas dvoch bojovníkov.
Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.
Stiahnuť
Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami
Stiahnuté 1x (7.22 kB)
Aplikácia je vrátane zdrojových kódov v jazyku Python