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

RPG 2: битката

Проблем

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

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

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

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

Кодът е на Python, а класовете са от първата част: 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 е абстрактен, за да няма „просто чудовище“. В Python това не става без абстрактен метод: ABC спира само клас, на който липсва абстрактен метод, а Monster пише всичко. Затова Monster("Сянка", 10, 1, "дух") ще се създаде. Никой в играта не го прави.

Падналият шаман е най-простото чудовище: само имена и числа. Касапинът презаписва атаката: след всеки удар силата му расте с 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# всяка е интерфейс: договор само с имената на методите, без код. Python няма отделна дума за това. Договорът е абстрактен клас само с абстрактни методи, а един клас може да наследи няколко такива: class Paladin(Character, Healable).

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

Виж го

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

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

Атаката пита целта: можеш ли да се защитаваш? isinstance(target, Defendable) е True, ако класът на обекта наследява Defendable. Тогава на target се вика defend. Превръщане, като в Java, не трябва: в Python всеки метод на обекта може да се извика направо. Иначе щетата се вади направо.

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

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

Пробвай

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

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

party = [Paladin("Ардан"), Barbarian("Грок"), Witch("Морена")]
for hero in party:
if isinstance(hero, Healable):
print(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.

Броячът е round_number, а не round: в Python round е име на вградена функция (закръгляне) и е добре да не се закрива.

hero = Paladin("Ардан")
monster = FallenShaman()
round_number = 1
while hero.is_alive() and monster.is_alive():
hero.attack(monster)
print(f"{round_number}. {hero.name} удря: {monster.name} има {monster.health}")
if monster.is_alive():
monster.attack(hero)
print(f" {monster.name} удря: {hero.name} има {hero.health}")
round_number += 1
print(f"Печели {hero.name}" if hero.is_alive() else f"Печели {monster.name}")
Стъпка по стъпка, с думи
  1. Ардан започва с 120 живот, Падналият шаман с 60. Броячът round_number започва от 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.is_alive() е 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. На кой ред в атаката е грешката?

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

Пробвай

Кое липсва?

С коя функция атаката пита дали целта наследява Defendable?
def attack(self, target):
    if (target, Defendable):
        target.defend(self._power)
    else:
        target.take_damage(self._power)

Упражнение

Още битки

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

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

Изход
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 героя с най-малко живот в списък от трима, вместо себе си.
  • Кой удря пръв? С Random(5) реши в началото на всеки рунд кой удря пръв.

Тест

Провери се

1. Защо Monster не наследява Character?
2. Скелетният крал наследява Defendable и спира по 5 от удар. Варварин със сила 20 го удря. Колко живот губи кралят?
3. Класът наследява Defendable и пише defend, но атаката вика направо take_damage. Какво става?
4. Какво прави isinstance(target, Defendable)?

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