Zum Inhalt
wannacode

RPG 2: die Schlacht

Kurz gesagt: Eine Schlacht in einem Rollenspiel mit Interfaces für „kann getroffen werden“, „kann sich verteidigen“ und „kann heilen“: ein Kampf auf Leben und Tod und ein Kampf über 10 Züge, in dem der Spieler wählt.

Problem

Gegen wen kämpfen die Helden?

Die Helden aus dem ersten Teil haben schon Leben und Fähigkeiten. Jetzt brauchen sie Feinde: die Monster, die aus den Tiefen von Arkanum aufsteigen. Der Gefallene Schamane ist schwach, der Skelettkönig steckt in einer Panzerung aus Knochen, und der Schlächter wird mit jedem Treffer stärker.

Ein Monster ist kein Held. Es hat keine Fähigkeit, und außerdem kämpfen Helden gegen Monster. Es kann also nicht von Character erben.

Aber der Treffer muss in beide Richtungen funktionieren. Und manche Kämpfer verteidigen sich: Der König blockt einen Teil des Schadens, der Barbar auch. Wenn der Angriff mit einem if für jede Klasse prüft, wer wer ist, braucht jedes neue Monster ein neues if.

Die Idee

Die Monster

Monster haben ihre eigene abstrakte Elternklasse. Sie sieht aus wie Character: ein Name, Leben, Kraft, ein Volk. Ihr Angriff trifft den Helden:

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

Der Gefallene Schamane ist das einfachste Monster: nur Namen und Zahlen. Der Schlächter überschreibt den Angriff: Nach jedem Treffer steigt seine Kraft um 1.

class FallenShaman extends Monster {
public FallenShaman() {
super("Gefallener Schamane", 60, 12, "Dämon");
}
}
class Butcher extends Monster {
public Butcher() {
super("Schlächter", 130, 10, "Dämon");
}
@Override
public void attack(Character target) {
super.attack(target);
power += 1;
}
}

Die Idee

Drei Verträge

Was Helden und Monster teilen, ist keine Elternklasse, sondern Fähigkeiten. Jede ist ein Interface:

interface IAttackable {
void takeDamage(int amount);
}
interface IDefendable {
void defend(int attackPower);
}
interface IHealable {
void heal(Character target, int amount);
}
  • IAttackable: kann getroffen werden. Sowohl Character als auch Monster implementieren es.
  • IDefendable: kann sich verteidigen. Die Verteidigung entscheidet, wie viel vom Treffer durchkommt. Jeder Held implementiert es über Character, und unter den Monstern tut das nur der Skelettkönig:
class SkeletonKing extends Monster implements IDefendable {
public SkeletonKing() {
super("Skelettkönig", 90, 16, "Untoter");
}
@Override
public void defend(int attackPower) {
takeDamage(Math.max(attackPower - 5, 0));
}
}
  • IHealable: kann heilen, auch sich selbst. Nur einige Helden implementieren es. Der Paladin heilt damit, und der Barbar hat es nicht, aber er überschreibt die Verteidigung: Er erleidet bei jedem Treffer ein Fünftel weniger.
class Paladin extends Character implements IHealable {
public Paladin(String name) {
super(name, 120, 14, "Mensch");
}
@Override
public void heal(Character target, int amount) {
target.restore(amount);
}
@Override
public String useAbility() {
heal(this, 20);
return name + " heilt sich: +20 Leben";
}
}
class Barbarian extends Character {
public Barbarian(String name) {
super(name, 110, 20, "Ork");
}
@Override
public void defend(int attackPower) {
takeDamage(attackPower - attackPower / 5);
}
@Override
public String useAbility() {
power += power / 2;
return name + " gerät in Rage: Kraft " + power;
}
}
Genauer, für Neugierige
  • In C# beginnen Interface-Namen mit I. In Java ist der übliche Stil ohne das I: Attackable, Defendable, Healable. Hier sind die Namen in beiden Sprachen gleich, damit du denselben Code liest.
  • Im ersten Teil hat der Barbar TakeDamage überschrieben. Jetzt hat die Verteidigung einen eigenen Namen. TakeDamage zieht den Schaden nur ab, und Defend entscheidet, wie viel Schaden durchkommt. So kann es auch Schaden geben, der an der Verteidigung vorbeigeht: Gift, eine Falle.
  • Restore steht in Character, damit jeder Held geheilt werden kann. Heilen kann nur, wer IHealable implementiert.

Sieh es dir an

Ein Treffer, zwei Ziele

Hier ist das neue Character. Neben den Verträgen hat es auch einen Angriff:

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

Der Angriff fragt das Ziel: Kannst du dich verteidigen? target is IDefendable defender ist true, wenn das Objekt das Interface implementiert. Dann ist defender dasselbe Objekt, aber als IDefendable, und du kannst darauf Defend aufrufen. Sonst wird der Schaden direkt abgezogen.

Grok der Barbar trifft den Schamanen und den König. Dann trifft ihn der Schamane:

Character grok = new Barbarian("Grok");
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());
Ausgabe
Gefallener Schamane: 40
Skelettkönig: 75
Grok: 100

Der Schamane verliert Groks volle Kraft, 20. Der König blockt 5 und verliert 15. Der Schamane trifft mit 12, und Grok erleidet 12 − 2 = 10. Der Angriff weiß nichts über den König oder den Barbaren: Er kennt nur den Vertrag.

Probier es aus

Wer kann heilen?

Was gibt das Programm aus? Die Klassen stammen von den Karten oben.

Character[] party = { new Paladin("Ardan"), new Barbarian("Grok"), new Witch("Morena") };
foreach (Character hero in party)
{
if (hero is IHealable)
{
Console.WriteLine(hero.Name);
}
}
Antwort

Sieh es dir an

Ein Kampf auf Leben und Tod

Ardan der Paladin gegen den Gefallenen Schamanen. Jede Runde besteht aus einem Treffer des Helden und, wenn das Monster noch lebt, einem Treffer des Monsters. Der Kampf endet, wenn einer von beiden fällt:

Character hero = new Paladin("Ardan");
Monster monster = new FallenShaman();
int round = 1;
while (hero.isAlive() && monster.isAlive()) {
hero.attack(monster);
System.out.println(round + ". " + hero.getName() + " trifft: " + monster.getName() + " hat " + monster.getHealth());
if (monster.isAlive()) {
monster.attack(hero);
System.out.println(" " + monster.getName() + " trifft: " + hero.getName() + " hat " + hero.getHealth());
}
round++;
}
System.out.println(hero.isAlive() ? hero.getName() + " gewinnt" : monster.getName() + " gewinnt");
Ausgabe
1. Ardan trifft: Gefallener Schamane hat 46
Gefallener Schamane trifft: Ardan hat 108
2. Ardan trifft: Gefallener Schamane hat 32
Gefallener Schamane trifft: Ardan hat 96
3. Ardan trifft: Gefallener Schamane hat 18
Gefallener Schamane trifft: Ardan hat 84
4. Ardan trifft: Gefallener Schamane hat 4
Gefallener Schamane trifft: Ardan hat 72
5. Ardan trifft: Gefallener Schamane hat 0
Ardan gewinnt

Die Schleife ist die Spielschleife aus der Lektion „Wie du über ein Spiel nachdenkst“: Der Zustand ist das Leben der beiden Kämpfer, die Regeln sind die Angriffe, und das Ende ist die Bedingung des while.

Character hero = new Paladin("Ardan");
Monster monster = new FallenShaman();
int round = 1;
while (hero.IsAlive() && monster.IsAlive())
{
hero.Attack(monster);
Console.WriteLine(round + ". " + hero.Name + " trifft: " + monster.Name + " hat " + monster.Health);
if (monster.IsAlive())
{
monster.Attack(hero);
Console.WriteLine(" " + monster.Name + " trifft: " + hero.Name + " hat " + hero.Health);
}
round++;
}
Console.WriteLine(hero.IsAlive() ? hero.Name + " gewinnt" : monster.Name + " gewinnt");
Schritt für Schritt, in Worten
  1. Ardan beginnt mit 120 Leben, Gefallener Schamane mit 60. Die Runden beginnen bei 1.
  2. Runde 1: Ardan trifft und nimmt 14 weg. Gefallener Schamane fällt von 60 auf 46.
  3. Gefallener Schamane lebt und schlägt zurück. Es nimmt 12 weg: Ardan fällt von 120 auf 108.
  4. Runde 2: Ardan trifft und nimmt 14 weg. Gefallener Schamane fällt von 46 auf 32.
  5. Gefallener Schamane lebt und schlägt zurück. Es nimmt 12 weg: Ardan fällt von 108 auf 96.
  6. Runde 3: Ardan trifft und nimmt 14 weg. Gefallener Schamane fällt von 32 auf 18.
  7. Gefallener Schamane lebt und schlägt zurück. Es nimmt 12 weg: Ardan fällt von 96 auf 84.
  8. Runde 4: Ardan trifft und nimmt 14 weg. Gefallener Schamane fällt von 18 auf 4.
  9. Gefallener Schamane lebt und schlägt zurück. Es nimmt 12 weg: Ardan fällt von 84 auf 72.
  10. Runde 5: Ardan trifft, und Gefallener Schamane fällt von 4 auf 0. monster.IsAlive() ist false, also schlägt es nicht zurück.
  11. Die Bedingung des while ist jetzt false: Einer von beiden lebt nicht mehr. Die Schleife hört auf, und Ardan gewinnt.

Sieh es dir an

10 Züge und die Wahl des Spielers

Gegen den Schlächter reichen Angriffe allein nicht. Deshalb wählt der Spieler in jedem Zug: 1 ist ein Angriff, 2 ist die Fähigkeit des Helden. Der Kampf dauert höchstens 10 Züge, und wenn am Ende beide noch leben, gewinnt der mit mehr Leben:

Scanner in = new Scanner(System.in);
Character hero = new Paladin("Ardan");
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() + " trifft: " + monster.getName() + " hat " + monster.getHealth());
}
if (monster.isAlive()) {
monster.attack(hero);
System.out.println(" " + monster.getName() + " trifft: " + hero.getName() + " hat " + hero.getHealth());
}
}
String winner = hero.getHealth() > monster.getHealth() ? hero.getName() : monster.getName();
System.out.println(winner + " gewinnt");
Ausgabe
1. Ardan trifft: Schlächter hat 116
Schlächter trifft: Ardan hat 110
2. Ardan trifft: Schlächter hat 102
Schlächter trifft: Ardan hat 99
3. Ardan trifft: Schlächter hat 88
Schlächter trifft: Ardan hat 87
4. Ardan heilt sich: +20 Leben
Schlächter trifft: Ardan hat 94
5. Ardan trifft: Schlächter hat 74
Schlächter trifft: Ardan hat 80
6. Ardan trifft: Schlächter hat 60
Schlächter trifft: Ardan hat 65
7. Ardan heilt sich: +20 Leben
Schlächter trifft: Ardan hat 69
8. Ardan trifft: Schlächter hat 46
Schlächter trifft: Ardan hat 52
9. Ardan heilt sich: +20 Leben
Schlächter trifft: Ardan hat 54
10. Ardan trifft: Schlächter hat 32
Schlächter trifft: Ardan hat 35
Ardan gewinnt

Die Eingabe für dieses Beispiel ist:

Eingabe (eine Wahl pro Zeile)
1 1 1 2 1 1 2 1 2 1

Wenn der Spieler nur Angriffe wählt, zehnmal 1, fällt der Paladin in Zug 9, und der Schlächter hat noch 4 Leben. Mit drei Heilungen reichen die Züge bis zum Ende, und der Paladin gewinnt mit 35 Leben gegen 32.

Genauer, für Neugierige
  • Die Wahl wird in jedem Zug gelesen, aber wenn der Held fällt, hört die Schleife auf, und die übrigen Eingabezeilen werden nicht gelesen.
  • choice == "2" vergleicht den Text. Alles andere, auch eine leere Zeile, ist ein Angriff: Das Spiel darf bei einer unbekannten Wahl nicht stehen bleiben. Wie du eine falsche Wahl mit einer Exception ablehnst, siehst du im dritten Teil.

Probier es aus

Finde den Fehler

Der Barbar trifft den Skelettkönig mit Kraft 20, und der König verliert 20 statt 15. In welcher Zeile des Angriffs steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Probier es aus

Was fehlt?

Mit welchem Wort fragt der Angriff, ob das Ziel das Interface implementiert?
public void Attack(Monster target)
{
    if (target  IDefendable defender)
    {
        defender.Defend(Power);
    }
    else
    {
        target.TakeDamage(Power);
    }
}

Übung

Mehr Schlachten

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

1. Ein neues Monster. Andariel, eine Dämonin, 100 Leben und 12 Kraft. Ihr Treffer ist vergiftet: Nach dem normalen Angriff erleidet das Ziel direkt über TakeDamage noch 4 Schaden, an der Verteidigung vorbei. Lass den Kampf auf Leben und Tod mit Grok dem Barbaren laufen.

Ausgabe
1. Grok trifft: Andariel hat 80
Andariel trifft: Grok hat 96
2. Grok trifft: Andariel hat 60
Andariel trifft: Grok hat 82
3. Grok trifft: Andariel hat 40
Andariel trifft: Grok hat 68
4. Grok trifft: Andariel hat 20
Andariel trifft: Grok hat 54
5. Grok trifft: Andariel hat 0
Grok gewinnt

Grok erleidet 10 durch den Treffer (12 minus ein Fünftel) und 4 durch das Gift: 14 pro Runde.

2. Ein Heiltrank. Eine Klasse Potion mit einer Menge Leben. Use(Character target) heilt das Ziel und gibt einen Text zurück. Der Trank kann einmal getrunken werden: Danach sagt er „Der Trank ist leer“. Im Kampf des Paladins gegen den Schlächter trinkt der Held einen Trank über 40, statt zuzuschlagen, wenn sein Leben unter 30 liegt.

Ausgabe
1. Ardan trifft: Schlächter hat 116
Schlächter trifft: Ardan hat 110
2. Ardan trifft: Schlächter hat 102
Schlächter trifft: Ardan hat 99
3. Ardan trifft: Schlächter hat 88
Schlächter trifft: Ardan hat 87
4. Ardan trifft: Schlächter hat 74
Schlächter trifft: Ardan hat 74
5. Ardan trifft: Schlächter hat 60
Schlächter trifft: Ardan hat 60
6. Ardan trifft: Schlächter hat 46
Schlächter trifft: Ardan hat 45
7. Ardan trifft: Schlächter hat 32
Schlächter trifft: Ardan hat 29
8. Ardan trinkt einen Trank: +40 Leben
Schlächter trifft: Ardan hat 52
9. Ardan trifft: Schlächter hat 18
Schlächter trifft: Ardan hat 34
10. Ardan trifft: Schlächter hat 4
Schlächter trifft: Ardan hat 15
11. Ardan trifft: Schlächter hat 0
Ardan gewinnt

3. Ein Kampfbericht. Statt einer Zeile für jeden Treffer gib am Ende aus, wie viele Runden der Kampf gedauert hat, wie viel Schaden der Held verursacht und wie viel er erlitten hat und wie viel Leben ihm bleibt. Grok der Barbar gegen den Skelettkönig:

Ausgabe
Runden: 6
Schaden verursacht: 90
Schaden erlitten: 65
Leben übrig: 45

Tipp: Merke dir das Leben des Ziels vor dem Treffer und ziehe sein Leben nach dem Treffer ab.

Weitere Übungen

  • Hexe gegen König. Lass den Kampf von Morena der Hexe gegen den Skelettkönig laufen, mit den Wahlen aus der Eingabe. Kann sie in 10 Zügen gewinnen?
  • Heiler der Gruppe. Der Paladin heilt in einem Array aus drei Helden den mit dem wenigsten Leben um 10, statt sich selbst.
  • Wer schlägt zuerst? Entscheide mit new Random(5) zu Beginn jeder Runde, wer zuerst zuschlägt.

Test

Teste dich

1. Warum erbt Monster nicht von Character?
2. Der Skelettkönig implementiert IDefendable und blockt 5 von jedem Treffer. Ein Barbar mit Kraft 20 trifft ihn. Wie viel Leben verliert der König?
3. Die Klasse implementiert IDefendable, aber der Angriff ruft takeDamage direkt auf. Was passiert?
4. Was macht target is IDefendable defender?

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