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

RPG 2: битката

Проблем

Срещу кого се бият героите?

Героите от първата част вече имат живот и умения. Сега им трябват врагове: чудовищата, които се надигат от дълбините на Арканум. Падналият шаман е слаб, Скелетният крал е покрит с костна броня, а Касапинът става по-силен с всеки удар.

Чудовището не е герой. То няма умение, а и героите се бият срещу чудовищата. Затова не може да наследи Character.

Но ударът трябва да работи и в двете посоки. А някои от участниците се защитават: кралят спира част от щетата, варваринът също. Ако атаката проверява кой е кой с if за всеки клас, всяко ново чудовище ще иска нов if.

Кодът е на JavaScript, а класовете са от първата част: Character, паладинът, варваринът и вещицата.

Идеята

Чудовищата

Чудовищата имат свой родител. Той прилича на Character: име, живот, сила, раса. Атаката му удря героя:

abstract class Monster implements IAttackable {
protected String name;
protected int health;
protected int power;
protected String race;
public Monster(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;
}
@Override
public void takeDamage(int amount) {
health = Math.max(health - amount, 0);
}
public boolean isAlive() {
return health > 0;
}
public void attack(Character target) {
target.defend(power);
}
}

В Java и C# Monster е абстрактен, за да няма „просто чудовище“. JavaScript няма абстрактни класове: new Monster(...) ще се създаде. Никой в играта не го прави.

Падналият шаман е най-простото чудовище: само имена и числа. Касапинът презаписва атаката: след всеки удар силата му расте с 1.

class FallenShaman extends Monster {
public FallenShaman() {
super("Падналият шаман", 60, 12, "демон");
}
}
class Butcher extends Monster {
public Butcher() {
super("Касапинът", 130, 10, "демон");
}
@Override
public void attack(Character target) {
super.attack(target);
power += 1;
}
}

Идеята

Три договора

Общото между героите и чудовищата не е родител, а способности. В Java и C# всяка е интерфейс: договор само с имената на методите, без код. JavaScript няма интерфейси. Договорът е самото име на метода: обект „може да се защитава“, ако има метод defend. Така се казва duck typing: „ако ходи като патица и квака като патица, значи е патица“.

interface IAttackable {
void takeDamage(int amount);
}
interface IDefendable {
void defend(int attackPower);
}
interface IHealable {
void heal(Character target, int amount);
}
  • takeDamage: може да бъде ударен. Имат го и Character, и Monster.
  • defend: може да се защитава. Защитата решава колко от удара да мине. Всеки герой го има през Character, а от чудовищата само Скелетният крал:
class SkeletonKing extends Monster implements IDefendable {
public SkeletonKing() {
super("Скелетният крал", 90, 16, "нежив");
}
@Override
public void defend(int attackPower) {
takeDamage(Math.max(attackPower - 5, 0));
}
}
  • heal: може да лекува, включително себе си. Само някои герои го имат. Паладинът лекува с него, а варваринът го няма, но презаписва защитата: от всеки удар поема с една пета по-малко.
class Paladin extends Character implements IHealable {
public Paladin(String name) {
super(name, 120, 14, "човек");
}
@Override
public void heal(Character target, int amount) {
target.restore(amount);
}
@Override
public String useAbility() {
heal(this, 20);
return name + " се лекува: +20 живот";
}
}
class Barbarian extends Character {
public Barbarian(String name) {
super(name, 110, 20, "орк");
}
@Override
public void defend(int attackPower) {
takeDamage(attackPower - attackPower / 5);
}
@Override
public String useAbility() {
power += power / 2;
return name + " изпада в ярост: сила " + power;
}
}
По-точно, за любопитните
  • Договорите не са написани никъде в кода, освен в коментара. Ако сбъркаш името, defense вместо defend, никой няма да те спре: проверката просто ще каже, че кралят не се защитава.
  • В първата част варваринът презаписваше takeDamage. Сега защитата има свое име. takeDamage само вади щетата, а defend решава колко щета да мине. Така ще може да има и щета, която минава покрай защитата: отрова, капан.
  • restore е в Character, за да може всеки герой да бъде лекуван. Да лекува може само онзи, който има heal.

Виж го

Един удар, две цели

Ето новият Character. Освен договорите, той има и атака:

abstract class Character implements IAttackable, IDefendable {
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;
}
@Override
public void takeDamage(int amount) {
health = Math.max(health - amount, 0);
}
@Override
public void defend(int attackPower) {
takeDamage(attackPower);
}
public void restore(int amount) {
health += amount;
}
public boolean isAlive() {
return health > 0;
}
public void attack(Monster target) {
if (target instanceof IDefendable defender) {
defender.defend(power);
} else {
target.takeDamage(power);
}
}
public abstract String useAbility();
}

Атаката пита целта: можеш ли да се защитаваш? typeof target.defend === "function" е true, ако обектът има метод defend. Ако го няма, target.defend е undefined, а typeof за него дава "undefined". Тогава щетата се вади направо.

Варваринът Грок удря шамана и краля. После шаманът удря него:

Character grok = new Barbarian("Грок");
Monster shaman = new FallenShaman();
Monster king = new SkeletonKing();
grok.attack(shaman);
grok.attack(king);
System.out.println(shaman.getName() + ": " + shaman.getHealth());
System.out.println(king.getName() + ": " + king.getHealth());
shaman.attack(grok);
System.out.println(grok.getName() + ": " + grok.getHealth());
Изход
Падналият шаман: 40
Скелетният крал: 75
Грок: 100

Шаманът губи цялата сила на Грок, 20. Кралят спира 5 и губи 15. Шаманът удря с 12, а Грок поема 12 − 2 = 10. Атаката не знае нищо за краля или варварина: тя знае само договора.

Пробвай

Кой може да лекува?

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

const party = [new Paladin("Ардан"), new Barbarian("Грок"), new Witch("Морена")];
for (const hero of party) {
if (typeof hero.heal === "function") {
console.log(hero.name);
}
}
Отговор

Виж го

Бой до смърт

Паладинът Ардан срещу Падналия шаман. Всеки рунд е удар на героя и, ако чудовището е още живо, удар на чудовището. Боят свършва, когато единият падне:

Character hero = new Paladin("Ардан");
Monster monster = new FallenShaman();
int round = 1;
while (hero.isAlive() && monster.isAlive()) {
hero.attack(monster);
System.out.println(round + ". " + hero.getName() + " удря: " + monster.getName() + " има " + monster.getHealth());
if (monster.isAlive()) {
monster.attack(hero);
System.out.println(" " + monster.getName() + " удря: " + hero.getName() + " има " + hero.getHealth());
}
round++;
}
System.out.println(hero.isAlive() ? "Печели " + hero.getName() : "Печели " + monster.getName());
Изход
1. Ардан удря: Падналият шаман има 46
Падналият шаман удря: Ардан има 108
2. Ардан удря: Падналият шаман има 32
Падналият шаман удря: Ардан има 96
3. Ардан удря: Падналият шаман има 18
Падналият шаман удря: Ардан има 84
4. Ардан удря: Падналият шаман има 4
Падналият шаман удря: Ардан има 72
5. Ардан удря: Падналият шаман има 0
Печели Ардан

Цикълът е цикълът на играта от урока „Как се мисли игра“: състоянието е животът на двамата, правилата са атаките, а краят е условието на while.

const hero = new Paladin("Ардан");
const monster = new FallenShaman();
let round = 1;
while (hero.isAlive() && monster.isAlive()) {
hero.attack(monster);
console.log(`${round}. ${hero.name} удря: ${monster.name} има ${monster.health}`);
if (monster.isAlive()) {
monster.attack(hero);
console.log(` ${monster.name} удря: ${hero.name} има ${hero.health}`);
}
round++;
}
console.log(hero.isAlive() ? `Печели ${hero.name}` : `Печели ${monster.name}`);
Стъпка по стъпка, с думи
  1. Ардан започва с 120 живот, Падналият шаман с 60. Рундовете започват от 1.
  2. Рунд 1: Ардан удря и отнема 14. Падналият шаман пада от 60 на 46.
  3. Падналият шаман е жив и отвръща. Отнема 12: Ардан пада от 120 на 108.
  4. Рунд 2: Ардан удря и отнема 14. Падналият шаман пада от 46 на 32.
  5. Падналият шаман е жив и отвръща. Отнема 12: Ардан пада от 108 на 96.
  6. Рунд 3: Ардан удря и отнема 14. Падналият шаман пада от 32 на 18.
  7. Падналият шаман е жив и отвръща. Отнема 12: Ардан пада от 96 на 84.
  8. Рунд 4: Ардан удря и отнема 14. Падналият шаман пада от 18 на 4.
  9. Падналият шаман е жив и отвръща. Отнема 12: Ардан пада от 84 на 72.
  10. Рунд 5: Ардан удря и Падналият шаман пада от 4 на 0. monster.isAlive() е false, затова не отвръща.
  11. Условието на while вече е false: единият не е жив. Цикълът спира и печели Ардан.

Виж го

10 хода и избор на играча

Срещу Касапина само атаките не стигат. Затова играчът избира всеки ход: 1 е атака, 2 е умението на героя. Боят е най-много 10 хода, а ако и двамата са живи накрая, печели по-здравият:

ask е помощникът за вход, който вече познаваш. Той и import-ът му са най-горе във файла и тук не се показват, а rl.close() е в края.

Scanner in = new Scanner(System.in);
Character hero = new Paladin("Ардан");
Monster monster = new Butcher();
for (int turn = 1; turn <= 10 && hero.isAlive() && monster.isAlive(); turn++) {
String choice = in.nextLine();
if (choice.equals("2")) {
System.out.println(turn + ". " + hero.useAbility());
} else {
hero.attack(monster);
System.out.println(turn + ". " + hero.getName() + " удря: " + monster.getName() + " има " + monster.getHealth());
}
if (monster.isAlive()) {
monster.attack(hero);
System.out.println(" " + monster.getName() + " удря: " + hero.getName() + " има " + hero.getHealth());
}
}
String winner = hero.getHealth() > monster.getHealth() ? hero.getName() : monster.getName();
System.out.println("Печели " + winner);
Изход
1. Ардан удря: Касапинът има 116
Касапинът удря: Ардан има 110
2. Ардан удря: Касапинът има 102
Касапинът удря: Ардан има 99
3. Ардан удря: Касапинът има 88
Касапинът удря: Ардан има 87
4. Ардан се лекува: +20 живот
Касапинът удря: Ардан има 94
5. Ардан удря: Касапинът има 74
Касапинът удря: Ардан има 80
6. Ардан удря: Касапинът има 60
Касапинът удря: Ардан има 65
7. Ардан се лекува: +20 живот
Касапинът удря: Ардан има 69
8. Ардан удря: Касапинът има 46
Касапинът удря: Ардан има 52
9. Ардан се лекува: +20 живот
Касапинът удря: Ардан има 54
10. Ардан удря: Касапинът има 32
Касапинът удря: Ардан има 35
Печели Ардан

Входът на този пример е:

Вход (по един избор на ред)
1 1 1 2 1 1 2 1 2 1

Ако играчът избере само атаки, десет пъти 1, паладинът пада на 9-ия ход, а на Касапина остават 4 живот. С три лечения ходовете стигат до края и паладинът печели с 35 живот срещу 32.

По-точно, за любопитните
  • Изборът се чете на всеки ход, но ако героят падне, цикълът спира и останалите редове на входа не се четат.
  • choice === "2" сравнява текста. Всичко друго, и празен ред, е атака: играта не може да спре на непознат избор. Как да откажеш неправилния избор с изключение, ще видиш в третата част.

Пробвай

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

Варваринът удря Скелетния крал със сила 20, а кралят губи 20 вместо 15. На кой ред в атаката е грешката?

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

Пробвай

Кое липсва?

С коя дума атаката пита какво е target.defend?
attack(target) {
  if ( target.defend === "function") {
    target.defend(this._power);
  } else {
    target.takeDamage(this._power);
  }
}

Упражнение

Още битки

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

1. Ново чудовище. Андариел, демон, 100 живот и 12 сила. Ударът ѝ е отровен: след обикновената атака целта поема още 4 щета направо през takeDamage, покрай защитата. Пусни боя до смърт с варварина Грок.

Изход
1. Грок удря: Андариел има 80
Андариел удря: Грок има 96
2. Грок удря: Андариел има 60
Андариел удря: Грок има 82
3. Грок удря: Андариел има 40
Андариел удря: Грок има 68
4. Грок удря: Андариел има 20
Андариел удря: Грок има 54
5. Грок удря: Андариел има 0
Печели Грок

Грок поема 10 от удара (12 минус една пета) и 4 от отровата: по 14 на рунд.

2. Лечебна отвара. Клас Potion с количество живот. use(target) лекува целта и връща текст. Отварата се пие веднъж: след това казва „Отварата е празна“. В боя на паладина с Касапина героят пие отвара за 40 вместо да удари, когато животът му е под 30.

Изход
1. Ардан удря: Касапинът има 116
Касапинът удря: Ардан има 110
2. Ардан удря: Касапинът има 102
Касапинът удря: Ардан има 99
3. Ардан удря: Касапинът има 88
Касапинът удря: Ардан има 87
4. Ардан удря: Касапинът има 74
Касапинът удря: Ардан има 74
5. Ардан удря: Касапинът има 60
Касапинът удря: Ардан има 60
6. Ардан удря: Касапинът има 46
Касапинът удря: Ардан има 45
7. Ардан удря: Касапинът има 32
Касапинът удря: Ардан има 29
8. Ардан пие отвара: +40 живот
Касапинът удря: Ардан има 52
9. Ардан удря: Касапинът има 18
Касапинът удря: Ардан има 34
10. Ардан удря: Касапинът има 4
Касапинът удря: Ардан има 15
11. Ардан удря: Касапинът има 0
Печели Ардан

3. Отчет за битката. Вместо ред за всеки удар, отпечатай накрая колко рунда е траял боят, колко щета е нанесъл героят, колко е поел и колко живот му остава. Варваринът Грок срещу Скелетния крал:

Изход
Рундове: 6
Нанесена щета: 90
Поета щета: 65
Остава живот: 45

Подсказка: запомни живота на целта преди удара и го извади от живота след удара.

Още задачи

  • Вещица срещу крал. Пусни боя на вещицата Морена със Скелетния крал с избор от входа. Може ли да спечели в 10 хода?
  • Лечител на отряда. Паладинът лекува с 10 героя с най-малко живот в масив от трима, вместо себе си.
  • Кой удря пръв? С makeRandom(5) реши в началото на всеки рунд кой удря пръв.

Тест

Провери се

1. Защо Monster не наследява Character?
2. Скелетният крал има defend и спира по 5 от удар. Варварин със сила 20 го удря. Колко живот губи кралят?
3. Класът има defend, но атаката вика направо takeDamage. Какво става?
4. Какво прави typeof target.defend === "function"?

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