Към съдържанието
wannacode

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();
}

Различно е само умението. Затова в Character го няма: всяка раса пише свой useAbility(). JavaScript няма абстрактни класове и методи. Ако някоя раса забрави умението, програмата спира чак когато то се извика: TypeError: hero.useAbility is not a function.

Умението връща текст за случилото се, вместо да печата. Така кой какво вижда на екрана решава играта, а не героят.

По-точно, за любопитните
  • Полетата започват с _. Това е договорка: „само за класа и наследниците му“. JavaScript има и истински скрити полета, с #, но тях не ги виждат и наследниците, затова общите полета тук са с _. Отвън се четат с get: hero.health чете живота, а hero.health = 1000 не го сменя, защото няма set. Във файл без import присвояването тихо не прави нищо. Във файл с import (модул, като програмите с ask) спира с TypeError: Cannot set property health of #<Character> which has only a getter.
  • В старото упражнение имаше getter и setter за всяко поле. Тук няма начин животът да се смени отвън. Той се мени само през takeDamage и уменията, затова никой не може да направи героя безсмъртен с един ред.
  • Math.max(this._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, сила 14

Променливата hero няма тип, а сочи обект от клас Paladin. hero.useAbility() вика метода на паладина, а showInfo и takeDamage идват от Character.

Идеята

Всяка раса по своему

Варваринът поема с една пета по-малко щета. Той презаписва 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 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 + " се готви да отскочи: следващият удар е наполовина";
}
}

И двамата ползват super.takeDamage: проверката, че животът не пада под 0, остава на едно място.

/ в JavaScript дава и дробно число: 24 / 5 е 4.8. Math.trunc отрязва дробната част, както целочисленото деление в Java. Отскокът на амазонката е в #dodging: истински скрито поле, само нейно, което не се вижда дори от Character. Методът се презаписва, като просто се напише пак в наследника: не трябва нищо отпред.

Пробвай

Колко живот остава?

Какво ще отпечата програмата? Character е от горната карта.

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} се готви да отскочи: следващият удар е наполовина`;
}
}
const lira = new Amazon("Лира");
lira.useAbility();
lira.takeDamage(30);
lira.takeDamage(30);
console.log(lira.health);
Отговор

Виж го

Отрядът

Некромантът и вещицата пишат само конструктор и умение:

class Necromancer extends Character {
public Necromancer(String name) {
super(name, 90, 10, "джудже");
}
@Override
public String useAbility() {
power += 3;
return name + " черпи сила от мъртвите: +3 сила";
}
}
class Witch extends Character {
public Witch(String name) {
super(name, 95, 16, "кървав елф");
}
@Override
public String useAbility() {
health += 15;
return 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, сила 16

Цикълът не знае кой е от коя раса. Всеки герой сам решава какво прави умението му и колко щета поема. Амазонката губи 15, варваринът 24, останалите 30. Шестата раса ще влезе в същия масив, без цикълът да се промени.

const 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();
}
Стъпка по стъпка, с думи
  1. Масивът party е един, а вътре са 5 обекта от 5 различни класа.
  2. hero сочи Ардан, обект Paladin. Изпълнява се useAbility() на Paladin. takeDamage е общият от Character: 140 − 30 = 110.
  3. hero сочи Лира, обект Amazon. Изпълнява се useAbility() на Amazon. Amazon презаписва takeDamage: от удара 30 поема само 15 и остават 85.
  4. hero сочи Грок, обект Barbarian. Изпълнява се useAbility() на Barbarian. Barbarian презаписва takeDamage: от удара 30 поема само 24 и остават 86.
  5. hero сочи Торин, обект Necromancer. Изпълнява се useAbility() на Necromancer. takeDamage е общият от Character: 90 − 30 = 60.
  6. hero сочи Морена, обект Witch. Изпълнява се useAbility() на Witch. takeDamage е общият от Character: 110 − 30 = 80.
  7. Цикълът е един и не знае расите. Кой метод се изпълнява, решава обектът, към който сочи hero.

Пробвай

Намери грешката

Програмата спира още при първия удар срещу варварина. На кой ред е грешката?

Избери реда с грешката.

Пробвай

Кое липсва?

JavaScript не може да задължи расата да има умение. Но паладинът трябва да вземе всичко общо от Character. Какво пише на празното място?
class Paladin  Character {
  constructor(name) {
    super(name, 120, 14, "човек");
  }

  useAbility() {
    this._health += 20;
    return `${this._name} се лекува: +20 живот`;
  }
}

Упражнение

Още герои

Реши задачите в редактора си. Започни от класовете в урока.

1. Шести герой. Добави друид от горския народ: 105 живот, 12 сила. Умението му дава +8 живот и +2 сила. Не пипай Character.

Изход
Ели, горски народ: живот 105, сила 12
Ели се свързва с гората: +8 живот, +2 сила
Ели, горски народ: живот 113, сила 14

2. Списък на отряда. Напиши функция showAll(party), която показва героите с номера и накрая общия им живот. Пусни я за отряда от урока.

Изход
1. Ардан, човек: живот 120, сила 14
2. Лира, елф: живот 100, сила 18
3. Грок, орк: живот 110, сила 20
4. Торин, джудже: живот 90, сила 10
5. Морена, кървав елф: живот 95, сила 16
Общо живот: 515

3. Кой е най-силен? Всеки от отряда ползва умението си веднъж. Напиши функция strongest(party), която връща героя с най-голяма сила.

Изход
Най-силен: Грок със сила 30

Още задачи

  • Отряд от входа. Прочети пет реда „раса име“, например „паладин Ардан“, и създай героите в масив. За непозната раса кажи това и прочети следващия ред.
  • Ярост с мярка. Ярост след ярост прави варварина безкрайно силен. Нека умението вдига силата само първия път, а после казва, че вече е в ярост.
  • Кой е най-здрав? Като задача 3, но по живот, след като всеки е поел удар от 25.

Тест

Провери се

1. Защо в Character няма useAbility()?
2. В масива party има обекти от пет класа. Кой takeDamage се вика за варварина в него?
3. Амазонката отскача, после я удрят с 40 и после пак с 40. Колко живот губи?
4. Barbarian презаписва takeDamage. Как вика версията на Character?

Създаден от Иво Гергов като част от кампанията на Evo Tech за достъпно обучение по програмиране ·