Zum Inhalt
wannacode

RPG 1: die Helden

Kurz gesagt: Vererbung in einem Spiel: Die fünf Heldenvölker erweitern eine abstrakte Klasse Character, jedes mit eigener Fähigkeit, und eine Schleife ruft sie alle auf.

Problem

Fünf Völker, eine Gruppe

In der Welt von Arkanum kämpfen fünf Völker um ihre Länder. Jedes hat seine eigenen Helden:

Volk Held Was sie auszeichnet
Menschen Paladin mittlerer Schaden, heilt sich
Elfen Amazone schnell, trifft härter als Menschen, weicht Treffern aus
Orks Barbar hoher Schaden, erleidet weniger Schaden, gerät in Rage
Zwerge Nekromant geringer Schaden, zieht Kraft aus den Toten
Blutelfen Hexe mittlerer Schaden, erholt sich mit der Kraft der Elemente

Du willst eine Gruppe aus allen fünf und eine Schleife, die sie anzeigt und in den Kampf schickt. Wenn jedes Volk eine eigene Klasse ohne Gemeinsamkeiten ist, hast du fünf Arrays, fünf Schleifen und fünf Kopien desselben Codes für Leben und Schaden. Und ein sechstes Volk will eine sechste Kopie.

Die Idee

Das Gemeinsame steht in Character

Jeder Held hat einen Namen, Leben, Kraft und ein Volk. Jeder Held erleidet Schaden, kann lebendig oder tot sein und kann zeigen, wer er ist. Das ist ihr Gemeinsames, und es kommt in die Elternklasse:

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 + ": Leben " + health + ", Kraft " + power);
}
public abstract String useAbility();
}

Nur die Fähigkeit ist verschieden. Deshalb ist use_ability() abstrakt: Jedes Volk muss sie schreiben. In Python sagst du das mit dem Dekorator @abstractmethod aus dem Modul abc, und Character erbt von ABC. Dadurch wird Character zu einer abstrakten Klasse, weil es keinen Helden gibt, der „nur ein Character“ ist: Character("Ardan", 100, 10, "Mensch") bricht mit TypeError: Can't instantiate abstract class Character without an implementation for abstract method 'use_ability' ab.

Die Fähigkeit gibt einen Text darüber zurück, was passiert ist, statt ihn auszugeben. So entscheidet das Spiel, wer was auf dem Bildschirm sieht, nicht der Held.

Genauer, für Neugierige
  • Die Felder beginnen mit _. Das ist eine Konvention: „nur für die Klasse und ihre Unterklassen“. Python erzwingt sie nicht, aber alle, die den Code lesen, halten sich daran. Von außen werden sie über @property gelesen: hero.health liest das Leben, und hero.health = 1000 bricht mit AttributeError ab.
  • Die alte Übung hatte für jedes Feld einen Getter und einen Setter. Hier gibt es keinen Weg, das Leben von außen zu ändern. Es ändert sich nur durch take_damage und die Fähigkeiten, also kann niemand einen Helden mit einer Zeile unsterblich machen.
  • max(self._health - amount, 0) verhindert, dass das Leben unter 0 fällt.

Sieh es dir an

Der erste Held

Im Konstruktor sagt der Paladin, zu welchem Volk er gehört und mit wie viel Leben und Kraft er beginnt. Alles andere übernimmt er von Character. Er schreibt nur seine Fähigkeit:

class Paladin extends Character {
public Paladin(String name) {
super(name, 120, 14, "Mensch");
}
@Override
public String useAbility() {
health += 20;
return name + " heilt sich: +20 Leben";
}
}
Character hero = new Paladin("Ardan");
hero.showInfo();
hero.takeDamage(30);
hero.showInfo();
System.out.println(hero.useAbility());
hero.showInfo();
Ausgabe
Ardan, Mensch: Leben 120, Kraft 14
Ardan, Mensch: Leben 90, Kraft 14
Ardan heilt sich: +20 Leben
Ardan, Mensch: Leben 110, Kraft 14

Die Variable hero hat keinen Typ; sie zeigt auf ein Objekt der Klasse Paladin. hero.use_ability() ruft die Methode des Paladins auf, und show_info und take_damage kommen aus Character.

Die Idee

Jedes Volk auf seine Art

Der Barbar erleidet ein Fünftel weniger Schaden. Er überschreibt take_damage und ruft die Version der Elternklasse mit der kleineren Zahl auf:

class Barbarian extends Character {
public Barbarian(String name) {
super(name, 110, 20, "Ork");
}
@Override
public void takeDamage(int amount) {
super.takeDamage(amount - amount / 5);
}
@Override
public String useAbility() {
power += power / 2;
return name + " gerät in Rage: Kraft " + power;
}
}

Die Amazone merkt sich, ob sie zum Ausweichen bereit ist. Wenn ja, wird der nächste Treffer halbiert, und das Ausweichen ist verbraucht:

class Amazon extends Character {
private boolean dodging;
public Amazon(String name) {
super(name, 100, 18, "Elf");
}
@Override
public void takeDamage(int amount) {
if (dodging) {
amount = amount / 2;
dodging = false;
}
super.takeDamage(amount);
}
@Override
public String useAbility() {
dodging = true;
return name + " macht sich zum Ausweichen bereit: der nächste Treffer wird halbiert";
}
}

Beide nutzen super().take_damage: Die Prüfung, dass das Leben nicht unter 0 fällt, bleibt an einer einzigen Stelle.

amount // 5 ist eine Ganzzahldivision: 30 // 5 ist 6, und 24 // 5 ist 4. Das Ausweichen der Amazone steckt in _dodging, einem Feld, das nur ihr gehört. Eine Methode überschreibst du, indem du sie in der Unterklasse einfach noch einmal schreibst: Davor ist nichts nötig.

Probier es aus

Wie viel Leben bleibt?

Was gibt das Programm aus? Character stammt von der Karte oben.

class Amazon(Character):
def __init__(self, name):
super().__init__(name, 100, 18, "Elf")
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} macht sich zum Ausweichen bereit: der nächste Treffer wird halbiert"
lira = Amazon("Lira")
lira.use_ability()
lira.take_damage(30)
lira.take_damage(30)
print(lira.health)
Antwort

Sieh es dir an

Die Gruppe

Der Nekromant und die Hexe schreiben nur einen Konstruktor und eine Fähigkeit:

class Necromancer extends Character {
public Necromancer(String name) {
super(name, 90, 10, "Zwerg");
}
@Override
public String useAbility() {
power += 3;
return name + " zieht Kraft aus den Toten: +3 Kraft";
}
}
class Witch extends Character {
public Witch(String name) {
super(name, 95, 16, "Blutelf");
}
@Override
public String useAbility() {
health += 15;
return name + " ruft die Elemente an: +15 Leben";
}
}

Jetzt kommen alle fünf in eine Liste, und eine Schleife lässt sie ihre Fähigkeit einsetzen und einen Treffer von 30 einstecken:

Character[] party = {
new Paladin("Ardan"),
new Amazon("Lira"),
new Barbarian("Grok"),
new Necromancer("Torin"),
new Witch("Morena"),
};
for (Character hero : party) {
System.out.println(hero.useAbility());
hero.takeDamage(30);
hero.showInfo();
}
Ausgabe
Ardan heilt sich: +20 Leben
Ardan, Mensch: Leben 110, Kraft 14
Lira macht sich zum Ausweichen bereit: der nächste Treffer wird halbiert
Lira, Elf: Leben 85, Kraft 18
Grok gerät in Rage: Kraft 30
Grok, Ork: Leben 86, Kraft 30
Torin zieht Kraft aus den Toten: +3 Kraft
Torin, Zwerg: Leben 60, Kraft 13
Morena ruft die Elemente an: +15 Leben
Morena, Blutelf: Leben 80, Kraft 16

Die Schleife weiß nicht, wer zu welchem Volk gehört. Jeder Held entscheidet selbst, was seine Fähigkeit bewirkt und wie viel Schaden er erleidet. Die Amazone verliert 15, der Barbar 24, die anderen 30. Ein sechstes Volk kommt in dieselbe Liste, ohne dass sich die Schleife ändert.

party = [
Paladin("Ardan"),
Amazon("Lira"),
Barbarian("Grok"),
Necromancer("Torin"),
Witch("Morena"),
]
for hero in party:
print(hero.use_ability())
hero.take_damage(30)
hero.show_info()
Schritt für Schritt, in Worten
  1. Es gibt eine Liste party, und es enthält 5 Objekte aus 5 verschiedenen Klassen.
  2. hero zeigt auf Ardan, ein Objekt der Klasse Paladin. Das use_ability() von Paladin läuft. take_damage ist die gemeinsame aus Character: 140 − 30 = 110.
  3. hero zeigt auf Lira, ein Objekt der Klasse Amazon. Das use_ability() von Amazon läuft. Amazon überschreibt take_damage: Vom Treffer mit 30 erleidet er nur 15, und es bleiben 85.
  4. hero zeigt auf Grok, ein Objekt der Klasse Barbarian. Das use_ability() von Barbarian läuft. Barbarian überschreibt take_damage: Vom Treffer mit 30 erleidet er nur 24, und es bleiben 86.
  5. hero zeigt auf Torin, ein Objekt der Klasse Necromancer. Das use_ability() von Necromancer läuft. take_damage ist die gemeinsame aus Character: 90 − 30 = 60.
  6. hero zeigt auf Morena, ein Objekt der Klasse Witch. Das use_ability() von Witch läuft. take_damage ist die gemeinsame aus Character: 110 − 30 = 80.
  7. Es gibt eine Schleife, und sie kennt die Völker nicht. Welche Methode läuft, entscheidet das Objekt, auf das hero zeigt.

Probier es aus

Finde den Fehler

Das Programm bricht schon beim ersten Treffer gegen den Barbaren ab. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Probier es aus

Was fehlt?

Was gehört in die Lücke, damit jedes Volk eine Fähigkeit haben muss?
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}: Leben {self._health}, Kraft {self._power}")

    @
    def use_ability(self):
        pass

Übung

Mehr Helden

Löse diese Aufgaben in deinem Editor. Beginne mit den Klassen aus der Lektion.

1. Ein sechster Held. Füge einen Druiden aus dem Waldvolk hinzu: 105 Leben, 12 Kraft. Seine Fähigkeit gibt +8 Leben und +2 Kraft. Rühre Character nicht an.

Ausgabe
Eli, Waldvolk: Leben 105, Kraft 12
Eli verbindet sich mit dem Wald: +8 Leben, +2 Kraft
Eli, Waldvolk: Leben 113, Kraft 14

2. Die Liste der Gruppe. Schreibe eine Funktion show_all(party), die die Helden mit Nummern anzeigt und am Ende ihr gesamtes Leben. Lass sie für die Gruppe aus der Lektion laufen.

Ausgabe
1. Ardan, Mensch: Leben 120, Kraft 14
2. Lira, Elf: Leben 100, Kraft 18
3. Grok, Ork: Leben 110, Kraft 20
4. Torin, Zwerg: Leben 90, Kraft 10
5. Morena, Blutelf: Leben 95, Kraft 16
Gesamtleben: 515

3. Wer ist der Stärkste? Jedes Mitglied der Gruppe setzt seine Fähigkeit einmal ein. Schreibe eine Funktion strongest(party), die den Helden mit der meisten Kraft zurückgibt.

Ausgabe
Stärkster: Grok mit Kraft 30

Weitere Übungen

  • Eine Gruppe aus der Eingabe. Lies fünf Zeilen der Form „Volk Name“, zum Beispiel „paladin Ardan“, und erzeuge die Helden in einer Liste. Bei einem unbekannten Volk sagst du das und liest die nächste Zeile.
  • Rage mit Maß. Rage um Rage macht den Barbaren endlos stark. Lass die Fähigkeit die Kraft nur beim ersten Mal erhöhen und sage danach, dass er schon in Rage ist.
  • Wer ist der Zäheste? Wie Aufgabe 3, aber nach Leben, nachdem jeder einen Treffer von 25 eingesteckt hat.

Test

Teste dich

1. Warum erbt Character von ABC, und warum ist use_ability mit @abstractmethod markiert?
2. Die Liste party enthält Objekte aus fünf Klassen. Welche take_damage wird darin für den Barbaren aufgerufen?
3. Die Amazone macht sich zum Ausweichen bereit, dann wird sie mit 40 getroffen und danach noch einmal mit 40. Wie viel Leben verliert sie?
4. Barbarian überschreibt take_damage. Wie ruft er die Version aus Character auf?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·