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}`); }}Различно е само умението. Затова useAbility() е абстрактен: всяка раса трябва да го напише. А Character е абстрактен клас, защото герой „просто персонаж“ няма.
Умението връща текст за случилото се, вместо да печата. Така кой какво вижда на екрана решава играта, а не героят.
По-точно, за любопитните
- Полетата са
protected: наследниците ги променят, а отвън се четат само с getter-и.
- В старото упражнение имаше getter и setter за всяко поле. Тук няма начин животът да се смени отвън. Той се мени само през
takeDamageи уменията, затова никой не може да направи героя безсмъртен с един ред. Math.max(health - amount, 0)пази живота да не падне под 0.
- Името
Characterвече има клас в Java:java.lang.Character, за символите. Свой клас със същото име в твоя файл го закрива. Работи, но ако ти трябва и стандартният, пиши го с цялото име:java.lang.Character.isDigit(c).
Виж го
Първият герой
Паладинът казва в конструктора каква е расата и с колко живот и сила започва. Всичко друго взима от 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Променливата е от тип Character, а обектът е Paladin. hero.useAbility() вика метода на паладина.
Идеята
Всяка раса по своему
Варваринът поема с една пета по-малко щета. Той презаписва takeDamage и вика версията на родителя с по-малкото число:
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.takeDamage: проверката, че животът не пада под 0, остава на едно място.
Пробвай
Колко живот остава?
Какво ще отпечата програмата? Character е от горната карта.
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 + " се готви да отскочи: следващият удар е наполовина"; }}
Character lira = new Amazon("Лира");lira.useAbility();lira.takeDamage(30);lira.takeDamage(30);System.out.println(lira.getHealth());Лира започва със 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 живот`; }}Сега петимата влизат в един масив от тип Character, а един цикъл ги кара да ползват умението си и да поемат удар от 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. Шестата раса ще влезе в същия масив, без цикълът да се промени.
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();}Стъпка по стъпка, с думи
- Масивът е от тип Character, а вътре са 5 обекта от 5 различни класа.
- hero сочи Ардан, обект Paladin. Изпълнява се useAbility() на Paladin. takeDamage е общият от Character: 140 − 30 = 110.
- hero сочи Лира, обект Amazon. Изпълнява се useAbility() на Amazon. Amazon презаписва takeDamage: от удара 30 поема само 15 и остават 85.
- hero сочи Грок, обект Barbarian. Изпълнява се useAbility() на Barbarian. Barbarian презаписва takeDamage: от удара 30 поема само 24 и остават 86.
- hero сочи Торин, обект Necromancer. Изпълнява се useAbility() на Necromancer. takeDamage е общият от Character: 90 − 30 = 60.
- hero сочи Морена, обект Witch. Изпълнява се useAbility() на Witch. takeDamage е общият от Character: 110 − 30 = 80.
- Цикълът е един и не знае расите. Кой метод се изпълнява, решава обектът, към който сочи hero.
Пробвай
Намери грешката
Програмата спира още при първия удар срещу варварина. На кой ред е грешката?
Избери реда с грешката.
super. методът вика себе си, не родителя. Това е рекурсия без дъно: takeDamage вика takeDamage, който пак вика... докато стекът не се напълни и програмата не спре с StackOverflowError. Трябва super.takeDamage(amount - amount / 5);.Пробвай
Кое липсва?
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 String useAbility();
}abstract: методът няма тяло, а всеки наследник трябва да го напише. Иначе компилаторът не го пуска. Затова и самият Character е abstract class.Упражнение
Още герои
Реши задачите в редактора си. Започни от класовете в урока.
1. Шести герой. Добави друид от горския народ: 105 живот, 12 сила. Умението му дава +8 живот и +2 сила. Не пипай Character.
Ели, горски народ: живот 105, сила 12Ели се свързва с гората: +8 живот, +2 силаЕли, горски народ: живот 113, сила 142. Списък на отряда. Напиши метод showAll(Character[] party), който показва героите с номера и накрая общия им живот. Пусни го за отряда от урока.
1. Ардан, човек: живот 120, сила 142. Лира, елф: живот 100, сила 183. Грок, орк: живот 110, сила 204. Торин, джудже: живот 90, сила 105. Морена, кървав елф: живот 95, сила 16Общо живот: 5153. Кой е най-силен? Всеки от отряда ползва умението си веднъж. Напиши метод Character strongest(Character[] party), който връща героя с най-голяма сила.
Най-силен: Грок със сила 30Още задачи
- Отряд от входа. Прочети пет реда „раса име“, например „паладин Ардан“, и създай героите в масив. За непозната раса кажи това и прочети следващия ред.
- Ярост с мярка. Ярост след ярост прави варварина безкрайно силен. Нека умението вдига силата само първия път, а после казва, че вече е в ярост.
- Кой е най-здрав? Като задача 3, но по живот, след като всеки е поел удар от 25.
Тест