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

RPG 2: битката

Проблем

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

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

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

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

Идеята

Чудовищата

Чудовищата имат свой абстрактен родител. Той прилича на 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);
}
}

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

Идеята

Три договора

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

interface IAttackable {
void takeDamage(int amount);
}
interface IDefendable {
void defend(int attackPower);
}
interface IHealable {
void heal(Character target, int amount);
}
  • IAttackable: може да бъде ударен. Реализират го и Character, и Monster.
  • IDefendable: може да се защитава. Защитата решава колко от удара да мине. Всеки герой го реализира през Character, а от чудовищата само Скелетният крал:
class SkeletonKing extends Monster implements IDefendable {
public SkeletonKing() {
super("Скелетният крал", 90, 16, "нежив");
}
@Override
public void defend(int attackPower) {
takeDamage(Math.max(attackPower - 5, 0));
}
}
  • IHealable: може да лекува, включително себе си. Само някои герои го реализират. Паладинът лекува с него, а варваринът го няма, но презаписва защитата: от всеки удар поема с една пета по-малко.
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;
}
}
По-точно, за любопитните
  • В C# имената на интерфейсите започват с I. В Java обичайното е без I: Attackable, Defendable, Healable. Тук имената са еднакви в двата езика, за да се чете един и същ код.
  • В първата част варваринът презаписваше TakeDamage. Сега защитата има свое име. TakeDamage само вади щетата, а Defend решава колко щета да мине. Така ще може да има и щета, която минава покрай защитата: отрова, капан.
  • Restore е в Character, за да може всеки герой да бъде лекуван. Да лекува може само онзи, който реализира IHealable.

Виж го

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

Ето новият 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();
}

Атаката пита целта: можеш ли да се защитаваш? target is IDefendable defender е true, ако обектът реализира интерфейса. Тогава defender е същият обект, но като IDefendable, и на него може да се извика Defend. Иначе щетата се вади направо.

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

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. Атаката не знае нищо за краля или варварина: тя знае само договора.

Пробвай

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

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

Character[] party = { new Paladin("Ардан"), new Barbarian("Грок"), new Witch("Морена") };
foreach (Character hero in party)
{
if (hero is IHealable)
{
Console.WriteLine(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.

Character hero = new Paladin("Ардан");
Monster monster = new FallenShaman();
int round = 1;
while (hero.IsAlive() && monster.IsAlive())
{
hero.Attack(monster);
Console.WriteLine(round + ". " + hero.Name + " удря: " + monster.Name + " има " + monster.Health);
if (monster.IsAlive())
{
monster.Attack(hero);
Console.WriteLine(" " + monster.Name + " удря: " + hero.Name + " има " + hero.Health);
}
round++;
}
Console.WriteLine(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 хода, а ако и двамата са живи накрая, печели по-здравият:

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. На кой ред в атаката е грешката?

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

Пробвай

Кое липсва?

С коя дума атаката пита дали целта реализира интерфейса?
public void Attack(Monster target)
{
    if (target  IDefendable defender)
    {
        defender.Defend(Power);
    }
    else
    {
        target.TakeDamage(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(Character 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 героя с най-малко живот в масив от трима, вместо себе си.
  • Кой удря пръв? С new Random(5) реши в началото на всеки рунд кой удря пръв.

Тест

Провери се

1. Защо Monster не наследява Character?
2. Скелетният крал реализира IDefendable и спира по 5 от удар. Варварин със сила 20 го удря. Колко живот губи кралят?
3. Класът реализира IDefendable, но атаката вика направо takeDamage. Какво става?
4. Какво прави target is IDefendable defender?

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