RPG 1: героите
Проблем
Пет раси, един отряд
В света на Арканум пет раси се борят за земите му. Всяка има свои герои:
| Раса | Герой | С какво се отличава |
|---|---|---|
| хора | паладин | средна щета, лекува се |
| елфи | амазонка | бърза, удря по-силно от хората, отскача от удари |
| орки | варварин | голяма щета, поема по-малко щета, изпада в ярост |
| джуджета | некромант | малка щета, черпи сила от мъртвите |
| кървави елфи | вещица | средна щета, възстановява се със силата на стихиите |
Искаш отряд от петимата и един цикъл, който ги показва и ги пуска в бой. Ако всяка раса е отделен клас без нищо общо, ще имаш пет масива, пет цикъла и пет копия на един и същ код за живота и щетата. А шестата раса ще иска шесто копие.
Идеята
Общото е в Character
Всеки герой има име, живот, сила и раса. Всеки поема щета, може да е жив или мъртъв и може да покаже какъв е. Това е общото и стои в родителя:
abstract class Character { protected String name; protected int health; protected int power; protected String race;
public Character(String name, int health, int power, String race) { this.name = name; this.health = health; this.power = power; this.race = race; }
public String getName() { return name; }
public int getHealth() { return health; }
public int getPower() { return power; }
public void takeDamage(int amount) { health = Math.max(health - amount, 0); }
public boolean isAlive() { return health > 0; }
public void showInfo() { System.out.println(name + ", " + race + ": живот " + health + ", сила " + power); }
public abstract String useAbility();}abstract class Character{ public string Name { get; protected set; } public int Health { get; protected set; } public int Power { get; protected set; } public string Race { get; protected set; }
public Character(string name, int health, int power, string race) { Name = name; Health = health; Power = power; Race = race; }
public virtual void TakeDamage(int amount) { Health = Math.Max(Health - amount, 0); }
public bool IsAlive() { return Health > 0; }
public void ShowInfo() { Console.WriteLine(Name + ", " + Race + ": живот " + Health + ", сила " + Power); }
public abstract string UseAbility();}from abc import ABC, abstractmethod
class Character(ABC): def __init__(self, name, health, power, race): self._name = name self._health = health self._power = power self._race = race
@property def name(self): return self._name
@property def health(self): return self._health
@property def power(self): return self._power
def take_damage(self, amount): self._health = max(self._health - amount, 0)
def is_alive(self): return self._health > 0
def show_info(self): print(f"{self._name}, {self._race}: живот {self._health}, сила {self._power}")
@abstractmethod def use_ability(self): passclass Character { constructor(name, health, power, race) { this._name = name; this._health = health; this._power = power; this._race = race; }
get name() { return this._name; }
get health() { return this._health; }
get power() { return this._power; }
takeDamage(amount) { this._health = Math.max(this._health - amount, 0); }
isAlive() { return this._health > 0; }
showInfo() { console.log(`${this._name}, ${this._race}: живот ${this._health}, сила ${this._power}`); }}Различно е само умението. Затова use_ability() е абстрактен: всяка раса трябва да го напише. В Python това казва декораторът @abstractmethod от модула abc, а Character наследява ABC. Така Character е абстрактен клас, защото герой „просто персонаж“ няма: Character("Ардан", 100, 10, "човек") спира с TypeError: Can't instantiate abstract class Character without an implementation for abstract method 'use_ability'.
Умението връща текст за случилото се, вместо да печата. Така кой какво вижда на екрана решава играта, а не героят.
По-точно, за любопитните
- Полетата започват с
_. Това е договорка: „само за класа и наследниците му“. Python не я пази, но всеки, който чете кода, я спазва. Отвън се четат през@property:hero.healthчете живота, аhero.health = 1000спира сAttributeError.
- В старото упражнение имаше getter и setter за всяко поле. Тук няма начин животът да се смени отвън. Той се мени само през
take_damageи уменията, затова никой не може да направи героя безсмъртен с един ред. max(self._health - amount, 0)пази живота да не падне под 0.
Виж го
Първият герой
Паладинът казва в конструктора каква е расата и с колко живот и сила започва. Всичко друго взима от Character. Пише само умението си:
class Paladin extends Character { public Paladin(String name) { super(name, 120, 14, "човек"); }
@Override public String useAbility() { health += 20; return name + " се лекува: +20 живот"; }}
Character hero = new Paladin("Ардан");hero.showInfo();hero.takeDamage(30);hero.showInfo();System.out.println(hero.useAbility());hero.showInfo();Ардан, човек: живот 120, сила 14Ардан, човек: живот 90, сила 14Ардан се лекува: +20 животАрдан, човек: живот 110, сила 14class Paladin : Character{ public Paladin(string name) : base(name, 120, 14, "човек") { }
public override string UseAbility() { Health += 20; return Name + " се лекува: +20 живот"; }}
Character hero = new Paladin("Ардан");hero.ShowInfo();hero.TakeDamage(30);hero.ShowInfo();Console.WriteLine(hero.UseAbility());hero.ShowInfo();Ардан, човек: живот 120, сила 14Ардан, човек: живот 90, сила 14Ардан се лекува: +20 животАрдан, човек: живот 110, сила 14class Paladin(Character): def __init__(self, name): super().__init__(name, 120, 14, "човек")
def use_ability(self): self._health += 20 return f"{self._name} се лекува: +20 живот"
hero = Paladin("Ардан")hero.show_info()hero.take_damage(30)hero.show_info()print(hero.use_ability())hero.show_info()Ардан, човек: живот 120, сила 14Ардан, човек: живот 90, сила 14Ардан се лекува: +20 животАрдан, човек: живот 110, сила 14class Paladin extends Character { constructor(name) { super(name, 120, 14, "човек"); }
useAbility() { this._health += 20; return `${this._name} се лекува: +20 живот`; }}
const hero = new Paladin("Ардан");hero.showInfo();hero.takeDamage(30);hero.showInfo();console.log(hero.useAbility());hero.showInfo();Ардан, човек: живот 120, сила 14Ардан, човек: живот 90, сила 14Ардан се лекува: +20 животАрдан, човек: живот 110, сила 14Променливата hero няма тип, а сочи обект от клас Paladin. hero.use_ability() вика метода на паладина, а show_info и take_damage идват от Character.
Идеята
Всяка раса по своему
Варваринът поема с една пета по-малко щета. Той презаписва take_damage и вика версията на родителя с по-малкото число:
class Barbarian extends Character { public Barbarian(String name) { super(name, 110, 20, "орк"); }
@Override public void takeDamage(int amount) { super.takeDamage(amount - amount / 5); }
@Override public String useAbility() { power += power / 2; return name + " изпада в ярост: сила " + power; }}class Barbarian : Character{ public Barbarian(string name) : base(name, 110, 20, "орк") { }
public override void TakeDamage(int amount) { base.TakeDamage(amount - amount / 5); }
public override string UseAbility() { Power += Power / 2; return Name + " изпада в ярост: сила " + Power; }}class Barbarian(Character): def __init__(self, name): super().__init__(name, 110, 20, "орк")
def take_damage(self, amount): super().take_damage(amount - amount // 5)
def use_ability(self): self._power += self._power // 2 return f"{self._name} изпада в ярост: сила {self._power}"class Barbarian extends Character { constructor(name) { super(name, 110, 20, "орк"); }
takeDamage(amount) { super.takeDamage(amount - Math.trunc(amount / 5)); }
useAbility() { this._power += Math.trunc(this._power / 2); return `${this._name} изпада в ярост: сила ${this._power}`; }}Амазонката помни дали се е приготвила да отскочи. Ако е, следващият удар е наполовина и отскокът свършва:
class Amazon extends Character { private boolean dodging;
public Amazon(String name) { super(name, 100, 18, "елф"); }
@Override public void takeDamage(int amount) { if (dodging) { amount = amount / 2; dodging = false; } super.takeDamage(amount); }
@Override public String useAbility() { dodging = true; return name + " се готви да отскочи: следващият удар е наполовина"; }}class Amazon : Character{ private bool dodging;
public Amazon(string name) : base(name, 100, 18, "елф") { }
public override void TakeDamage(int amount) { if (dodging) { amount = amount / 2; dodging = false; } base.TakeDamage(amount); }
public override string UseAbility() { dodging = true; return Name + " се готви да отскочи: следващият удар е наполовина"; }}class Amazon(Character): def __init__(self, name): super().__init__(name, 100, 18, "елф") self._dodging = False
def take_damage(self, amount): if self._dodging: amount = amount // 2 self._dodging = False super().take_damage(amount)
def use_ability(self): self._dodging = True return f"{self._name} се готви да отскочи: следващият удар е наполовина"class Amazon extends Character { #dodging = false;
constructor(name) { super(name, 100, 18, "елф"); }
takeDamage(amount) { if (this.#dodging) { amount = Math.trunc(amount / 2); this.#dodging = false; } super.takeDamage(amount); }
useAbility() { this.#dodging = true; return `${this._name} се готви да отскочи: следващият удар е наполовина`; }}И двамата ползват super().take_damage: проверката, че животът не пада под 0, остава на едно място.
amount // 5 е целочислено деление: 30 // 5 е 6, а 24 // 5 е 4. Отскокът на амазонката е в _dodging, само нейно поле. Методът се презаписва, като просто се напише пак в наследника: не трябва нищо отпред.
Пробвай
Колко живот остава?
Какво ще отпечата програмата? Character е от горната карта.
class Amazon(Character): def __init__(self, name): super().__init__(name, 100, 18, "елф") self._dodging = False
def take_damage(self, amount): if self._dodging: amount = amount // 2 self._dodging = False super().take_damage(amount)
def use_ability(self): self._dodging = True return f"{self._name} се готви да отскочи: следващият удар е наполовина"
lira = Amazon("Лира")lira.use_ability()lira.take_damage(30)lira.take_damage(30)print(lira.health)Лира започва със 100. Умението ѝ вдига отскока. Първият удар е 30 / 2 = 15 и отскокът свършва: 85. Вторият е цял: 85 − 30 = 55.
Виж го
Отрядът
Некромантът и вещицата пишат само конструктор и умение:
class Necromancer extends Character { public Necromancer(String name) { super(name, 90, 10, "джудже"); }
@Override public String useAbility() { power += 3; return name + " черпи сила от мъртвите: +3 сила"; }}class Necromancer : Character{ public Necromancer(string name) : base(name, 90, 10, "джудже") { }
public override string UseAbility() { Power += 3; return Name + " черпи сила от мъртвите: +3 сила"; }}class Necromancer(Character): def __init__(self, name): super().__init__(name, 90, 10, "джудже")
def use_ability(self): self._power += 3 return f"{self._name} черпи сила от мъртвите: +3 сила"class Necromancer extends Character { constructor(name) { super(name, 90, 10, "джудже"); }
useAbility() { this._power += 3; return `${this._name} черпи сила от мъртвите: +3 сила`; }}class Witch extends Character { public Witch(String name) { super(name, 95, 16, "кървав елф"); }
@Override public String useAbility() { health += 15; return name + " призовава стихиите: +15 живот"; }}class Witch : Character{ public Witch(string name) : base(name, 95, 16, "кървав елф") { }
public override string UseAbility() { Health += 15; return Name + " призовава стихиите: +15 живот"; }}class Witch(Character): def __init__(self, name): super().__init__(name, 95, 16, "кървав елф")
def use_ability(self): self._health += 15 return f"{self._name} призовава стихиите: +15 живот"class Witch extends Character { constructor(name) { super(name, 95, 16, "кървав елф"); }
useAbility() { this._health += 15; return `${this._name} призовава стихиите: +15 живот`; }}Сега петимата влизат в един списък, а един цикъл ги кара да ползват умението си и да поемат удар от 30:
Character[] party = { new Paladin("Ардан"), new Amazon("Лира"), new Barbarian("Грок"), new Necromancer("Торин"), new Witch("Морена"),};for (Character hero : party) { System.out.println(hero.useAbility()); hero.takeDamage(30); hero.showInfo();}Ардан се лекува: +20 животАрдан, човек: живот 110, сила 14Лира се готви да отскочи: следващият удар е наполовинаЛира, елф: живот 85, сила 18Грок изпада в ярост: сила 30Грок, орк: живот 86, сила 30Торин черпи сила от мъртвите: +3 силаТорин, джудже: живот 60, сила 13Морена призовава стихиите: +15 животМорена, кървав елф: живот 80, сила 16Character[] party ={ new Paladin("Ардан"), new Amazon("Лира"), new Barbarian("Грок"), new Necromancer("Торин"), new Witch("Морена"),};foreach (Character hero in party){ Console.WriteLine(hero.UseAbility()); hero.TakeDamage(30); hero.ShowInfo();}Ардан се лекува: +20 животАрдан, човек: живот 110, сила 14Лира се готви да отскочи: следващият удар е наполовинаЛира, елф: живот 85, сила 18Грок изпада в ярост: сила 30Грок, орк: живот 86, сила 30Торин черпи сила от мъртвите: +3 силаТорин, джудже: живот 60, сила 13Морена призовава стихиите: +15 животМорена, кървав елф: живот 80, сила 16party = [ Paladin("Ардан"), Amazon("Лира"), Barbarian("Грок"), Necromancer("Торин"), Witch("Морена"),]for hero in party: print(hero.use_ability()) hero.take_damage(30) hero.show_info()Ардан се лекува: +20 животАрдан, човек: живот 110, сила 14Лира се готви да отскочи: следващият удар е наполовинаЛира, елф: живот 85, сила 18Грок изпада в ярост: сила 30Грок, орк: живот 86, сила 30Торин черпи сила от мъртвите: +3 силаТорин, джудже: живот 60, сила 13Морена призовава стихиите: +15 животМорена, кървав елф: живот 80, сила 16const party = [ new Paladin("Ардан"), new Amazon("Лира"), new Barbarian("Грок"), new Necromancer("Торин"), new Witch("Морена"),];for (const hero of party) { console.log(hero.useAbility()); hero.takeDamage(30); hero.showInfo();}Ардан се лекува: +20 животАрдан, човек: живот 110, сила 14Лира се готви да отскочи: следващият удар е наполовинаЛира, елф: живот 85, сила 18Грок изпада в ярост: сила 30Грок, орк: живот 86, сила 30Торин черпи сила от мъртвите: +3 силаТорин, джудже: живот 60, сила 13Морена призовава стихиите: +15 животМорена, кървав елф: живот 80, сила 16Цикълът не знае кой е от коя раса. Всеки герой сам решава какво прави умението му и колко щета поема. Амазонката губи 15, варваринът 24, останалите 30. Шестата раса ще влезе в същия списък, без цикълът да се промени.
party = [ Paladin("Ардан"), Amazon("Лира"), Barbarian("Грок"), Necromancer("Торин"), Witch("Морена"),]for hero in party: print(hero.use_ability()) hero.take_damage(30) hero.show_info()Стъпка по стъпка, с думи
- Списъкът party е един, а вътре са 5 обекта от 5 различни класа.
- hero сочи Ардан, обект Paladin. Изпълнява се use_ability() на Paladin. take_damage е общият от Character: 140 − 30 = 110.
- hero сочи Лира, обект Amazon. Изпълнява се use_ability() на Amazon. Amazon презаписва take_damage: от удара 30 поема само 15 и остават 85.
- hero сочи Грок, обект Barbarian. Изпълнява се use_ability() на Barbarian. Barbarian презаписва take_damage: от удара 30 поема само 24 и остават 86.
- hero сочи Торин, обект Necromancer. Изпълнява се use_ability() на Necromancer. take_damage е общият от Character: 90 − 30 = 60.
- hero сочи Морена, обект Witch. Изпълнява се use_ability() на Witch. take_damage е общият от Character: 110 − 30 = 80.
- Цикълът е един и не знае расите. Кой метод се изпълнява, решава обектът, към който сочи hero.
Пробвай
Намери грешката
Програмата спира още при първия удар срещу варварина. На кой ред е грешката?
Избери реда с грешката.
self. сочи самия варварин, затова методът вика себе си, не родителя. Това е рекурсия без дъно: take_damage вика take_damage, който пак вика... докато стекът не се напълни и програмата не спре с RecursionError: maximum recursion depth exceeded. Трябва super().take_damage(amount - amount // 5).Пробвай
Кое липсва?
from abc import ABC, abstractmethod
class Character(ABC):
def __init__(self, name, health, power, race):
self._name = name
self._health = health
self._power = power
self._race = race
@property
def name(self):
return self._name
@property
def health(self):
return self._health
@property
def power(self):
return self._power
def take_damage(self, amount):
self._health = max(self._health - amount, 0)
def is_alive(self):
return self._health > 0
def show_info(self):
print(f"{self._name}, {self._race}: живот {self._health}, сила {self._power}")
@
def use_ability(self):
passabstractmethod: методът е само обещание, а всеки наследник трябва да го напише. Иначе обект от наследника не може да се създаде и Python спира с TypeError. За да работи, Character наследява ABC.Упражнение
Още герои
Реши задачите в редактора си. Започни от класовете в урока.
1. Шести герой. Добави друид от горския народ: 105 живот, 12 сила. Умението му дава +8 живот и +2 сила. Не пипай Character.
Ели, горски народ: живот 105, сила 12Ели се свързва с гората: +8 живот, +2 силаЕли, горски народ: живот 113, сила 142. Списък на отряда. Напиши функция show_all(party), която показва героите с номера и накрая общия им живот. Пусни я за отряда от урока.
1. Ардан, човек: живот 120, сила 142. Лира, елф: живот 100, сила 183. Грок, орк: живот 110, сила 204. Торин, джудже: живот 90, сила 105. Морена, кървав елф: живот 95, сила 16Общо живот: 5153. Кой е най-силен? Всеки от отряда ползва умението си веднъж. Напиши функция strongest(party), която връща героя с най-голяма сила.
Най-силен: Грок със сила 30Още задачи
- Отряд от входа. Прочети пет реда „раса име“, например „паладин Ардан“, и създай героите в списък. За непозната раса кажи това и прочети следващия ред.
- Ярост с мярка. Ярост след ярост прави варварина безкрайно силен. Нека умението вдига силата само първия път, а после казва, че вече е в ярост.
- Кой е най-здрав? Като задача 3, но по живот, след като всеки е поел удар от 25.
Тест