Zum Inhalt
wannacode

Helden, Feinde, Werte

Kurz gesagt: Ein Wesen für alle: Held und Feinde unterscheiden sich durch Daten (Zeilen einer Tabelle) und durch zwei Methoden, und jeder Wert ist eine Summe aus Schichten, Basis + Ausrüstung + Effekte, die bei jeder Anfrage neu berechnet wird.

Problem

Zehn Feinde, drei Klassen, eine Frage

Im Spiel gibt es zehn Arten von Feinden und drei Heldenklassen. Alle haben Leben, Stärke, Geschick und Verteidigung, und ein Treffer fragt bei allen dieselben Zahlen ab. Wie schreibt man so etwas?

Der erste Einfall von jedem, der Vererbung gelernt hat, ist eine Klasse für jede Art: Enemy ist der Elternteil, Goblin und Skeleton sind die Kinder. Bald sieht man den Baum:

Enemy
├── Rat
├── Goblin
│ ├── GoblinSpearman
│ └── GoblinShaman
├── Undead
│ ├── Skeleton immun gegen Blutung und Gift
│ └── StoneServant immun gegen Blutung und Gift (schon wieder!)
├── Golem immun gegen Blutung und Gift (schon wieder!!)
└── ...

Das Skelett, der Steindiener und der Golem bluten nicht und lassen sich nicht vergiften, gehören aber nicht zu einer Familie: Eines ist untot, die anderen sind aus Stein. Entweder wird die Immunität in drei Klassen kopiert, oder du brauchst dazwischen eine Klasse „Blutlos“, die die Familie „Untot“ kreuzt. Und eine zweite Eigenschaft (schwach gegen Feuer) wäre eine weitere kreuzende Familie. Ein tiefer Baum ist gut, solange jeder Fall in einen Zweig passt.

Es gibt noch ein Problem: Ein neuer Feind bedeutet eine neue Klasse, neuen kompilierten Code und einen Programmierer. Ein Designer, der einen Goblin mit 30 Leben will, kann ihn nicht allein machen.

In RPG 1 erben die fünf Völker von einem Character. Dort ist die Vererbung am richtigen Platz, weil jedes Volk etwas anderes tut (seine eigene Fähigkeit). Hier unterscheiden sich die Feinde in Zahlen. Anderes Verhalten gehört in eine Methode. Andere Zahlen sind Daten.

Die Idee

Ein Feind ist eine Zeile in einer Tabelle

Die erste Regel des Spiels: Wenn sich zwei Dinge nur in Zahlen unterscheiden, sind sie eine Klasse und zwei Zeilen Daten. Die Feinde dieser Lektion sind vier Textzeilen, die Zellen sind durch | getrennt:

# id|Name|Leben|Stärke|Geschick|Verteidigung|immun
rat|Riesenratte|9|1|6|0|-
goblin|Speergoblin|16|2|5|1|-
skeleton|Skelett|28|4|4|2|bleed,poison
golem|Erzgolem|46|6|1|4|bleed,poison

Das Spiel arbeitet genau so, nur mit zehn Feinden. Hier ist die echte Tabelle aus dem Canon. Die Spalten, die diese Lektion nicht nutzt (Zeichen, Farbe, Schaden, Erfahrung, Gold, Beute, Fähigkeit, Tiefe), sind weggelassen, die Zeilen sind dieselben:

# id|Name|Leben|Stärke|Geschick|Verteidigung|Tempo|resistent|schwach|immun|Stimmung
rat|Riesenratte|9|1|6|0|12|-|-|-|wander
goblin|Speergoblin|16|2|5|1|10|-|-|-|wander
shaman|Goblin-Schamane|13|1|4|0|9|-|-|-|sleep
wolf|Blutwolf|20|3|8|1|14|-|-|-|sleep
wolf_leader|Wolfsführer|36|4|9|2|14|-|-|-|sleep
skeleton|Skelett|28|4|4|2|10|slash|blunt|bleed,poison|sleep
cultist|Kultist|26|3|6|1|11|-|-|-|wander
golem|Erzgolem|46|6|1|4|7|pierce,fire|blunt|bleed,poison|sleep
servant|Steindiener|14|2|3|1|9|-|blunt|bleed,poison|sleep
warden|Wächter der Tiefe|120|5|4|3|10|-|-|bleed,poison|guard

Der Code liest die Tabelle beim Start und macht für jede Zeile einen Datensatz. Ein neuer Feind ist eine neue Zeile: Keine einzige Codezeile ändert sich. Das nennt man datengetrieben (data-driven): Das Spiel ist eine Maschine, und der Inhalt steht in Tabellen, die ohne Programmierer bearbeitet werden können.

Genauer, für Neugierige

So werden die Tabellen der Lektion (Feinde, Gegenstände und Effekte) gelesen. Der Text ist eine Konstante, und rows zerlegt ihn in Zeilen und Zellen und überspringt leere Zeilen und Notizen, die mit # beginnen:

record Item(String id, String name, String slot, Map<String, Integer> mods) {}
record Effect(String id, String name, Map<String, Integer> mods) {}
record Kind(String id, String name, int hp, int strength, int agility, int defense, List<String> immune) {}
final class Data {
static final String[] STATS = { "maxHp", "strength", "agility", "defense", "evasion" };
static final String[] SLOTS = { "armor", "ring" };
static final String ITEMS_TEXT = """
# id|Name|Slot|Modifikatoren
leather|Lederrüstung|armor|defense:2
chain|Kettenhemd|armor|defense:4,evasion:-5
ring_strength|Ring der Stärke|ring|strength:2
ring_life|Ring des Lebens|ring|maxHp:10""";
static final String EFFECTS_TEXT = """
# id|Name|Modifikatoren
rage|Raserei|strength:3,defense:-2
shield|Schild|defense:3
weak|Schwäche|strength:-4""";
static final String ENEMIES_TEXT = """
# id|Name|Leben|Stärke|Geschick|Verteidigung|immun
rat|Riesenratte|9|1|6|0|-
goblin|Speergoblin|16|2|5|1|-
skeleton|Skelett|28|4|4|2|bleed,poison
golem|Erzgolem|46|6|1|4|bleed,poison""";
static final Map<String, Item> ITEMS = new LinkedHashMap<>();
static final Map<String, Effect> EFFECTS = new LinkedHashMap<>();
static final Map<String, Kind> KINDS = new LinkedHashMap<>();
static {
for (String[] row : rows(ITEMS_TEXT)) {
ITEMS.put(row[0], new Item(row[0], row[1], row[2], parseMods(row[3])));
}
for (String[] row : rows(EFFECTS_TEXT)) {
EFFECTS.put(row[0], new Effect(row[0], row[1], parseMods(row[2])));
}
for (String[] row : rows(ENEMIES_TEXT)) {
List<String> immune = row[6].equals("-") ? List.of() : List.of(row[6].split(","));
KINDS.put(row[0], new Kind(row[0], row[1], Integer.parseInt(row[2]), Integer.parseInt(row[3]),
Integer.parseInt(row[4]), Integer.parseInt(row[5]), immune));
}
}
static List<String[]> rows(String table) {
List<String[]> result = new ArrayList<>();
for (String line : table.split("\n")) {
String text = line.trim();
if (text.isEmpty() || text.startsWith("#")) continue;
result.add(text.split("\\|"));
}
return result;
}
static Map<String, Integer> parseMods(String text) {
Map<String, Integer> mods = new HashMap<>();
if (text.equals("-")) return mods;
for (String part : text.split(",")) {
String[] pair = part.split(":");
mods.put(pair[0], Integer.parseInt(pair[1]));
}
return mods;
}
}
Ein record ist ein Datensatz mit Feldern, die sich nicht ändern. LinkedHashMap behält die Einfügereihenfolge, deshalb kommen die Feinde in der Reihenfolge der Tabelle heraus.

Die Idee

Ein Wesen für alle

Die Unterschiede zwischen Held und Feinden sind klein. Deshalb gibt es eine Klasse Creature mit allem Gemeinsamen: einem Namen, Leben, einer Basis (eine Zahl für jeden Wert) und einer Liste von Effekten. Hier ist ihr Anfang:

class Creature {
final String name;
final Map<String, Integer> base = new HashMap<>();
final List<Effect> effects = new ArrayList<>();
int hp;
Creature(String name) {
this.name = name;
for (String stat : Data.STATS) base.put(stat, 0);
}

Creature hat eine Tabelle von Werten und eine Liste von Effekten; es ist kein Goblin und kein Skelett. Das ist Komposition: Du setzt ein Wesen aus Teilen zusammen, statt es von einem allgemeineren Wesen zu erben. Ein Feind aus der Tabelle füllt die Basis einfach aus seiner Zeile:

class Enemy extends Creature {
final Kind kind;
Enemy(Kind kind) {
super(kind.name());
this.kind = kind;
base.put("maxHp", kind.hp());
base.put("strength", kind.strength());
base.put("agility", kind.agility());
base.put("defense", kind.defense());
hp = kind.hp();
}
// Immunität sind Daten: eine Liste aus der Tabellenzeile.
@Override
boolean isImmune(String effectId) {
return kind.immune().contains(effectId);
}
}

Und der Held trägt noch etwas mehr: Ausrüstung.

class Hero extends Creature {
final Map<String, String> equipment = new HashMap<>();
Hero(String name, int maxHp, int strength, int agility, int defense) {
super(name);
base.put("maxHp", maxHp);
base.put("strength", strength);
base.put("agility", agility);
base.put("defense", defense);
hp = maxHp;
for (String slot : Data.SLOTS) equipment.put(slot, "");
}

Es bleibt eine Ebene Vererbung: Creature → Hero und Enemy. Die beiden Unterklassen ersetzen je eine Methode, weil nur dort das Verhalten verschieden ist: Der Held hat gearBonus (Ausrüstung), der Feind hat isImmune (Immunität aus seiner Zeile). Alles andere, auch alles im Kampfsystem, fragt ein Creature und weiß nicht, wen es fragt.

Im Canon ist es dasselbe. Hier sind der Konstruktor von Enemy und sein isImmune:

Enemy(int id, EnemyKind kind) {
super(id, nm(kind));
this.kind = kind;
this.base.put("maxHp", kind.hp);
this.base.put("strength", kind.strength);
this.base.put("agility", kind.agility);
this.base.put("defense", kind.defense);
this.base.put("speed", kind.speed);
this.hp = kind.hp;
this.state = kind.mood;
this.calm = 0;
this.lostTurns = 0;
this.phase = 1;
this.dead = false;
this.summoned = false;
this.noticed = false;
}
@Override
boolean isImmune(String effectId) {
return this.kind.immune.indexOf(effectId) >= 0;
}

Ein Feind aus der Tabelle kopiert seine Zahlen in base und behält eine Referenz auf seine Zeile (kind). Immunität ist keine Klasse, sondern eine Liste in dieser Zeile.

Genauer, für Neugierige

Die Regel „Komposition statt Vererbung“ sagt nicht „erbe nie“. Sie sagt: Erbe für Verhalten (eine Methode ist anders), setze für den Aufbau zusammen (die Teile sind anders). Hier hält Creature das gemeinsame Gerüst, und die Unterklassen sind zwei dünne. Wären die Helden fünf Völker mit fünf Fähigkeiten wie in RPG 1, wäre Vererbung das richtige Werkzeug.

Im Canon sind auch die Effekte kleine Klassen (PoisonEffect, RageEffect), weil Gift und Brennen jeden Zug etwas tun. Ein Effekt, der nur Zahlen ändert, kann auch eine Tabellenzeile sein, wie in der Lektion.

Sieh es dir an

Alle durch einen Code

Der Held und die vier Feinde kommen in eine Liste von Creature. Die Schleife fragt nicht, wer wer ist: Sie ruft maxHp, stat und evasion auf und hängt bei einem Feind, der gegen Blutung immun ist, eine Notiz an. Danach bekommen alle denselben Effekt.

List<Creature> all = new ArrayList<>();
all.add(new Hero("Barbar", 48, 7, 4, 3));
for (Kind kind : Data.KINDS.values()) all.add(new Enemy(kind));
for (Creature c : all) {
String line = c.name + ": Leben " + c.maxHp() + ", Stärke " + c.stat("strength") + ", Verteidigung " + c.stat("defense") + ", Ausweichen " + c.evasion();
if (c.isImmune("bleed")) line += ", immun gegen Blutung und Gift";
System.out.println(line);
}
System.out.println("Schwäche für alle:");
for (Creature c : all) {
int before = c.stat("strength");
c.addEffect("weak");
System.out.println(c.name + ": Stärke " + before + " -> " + c.stat("strength"));
}
Ausgabe
Barbar: Leben 48, Stärke 7, Verteidigung 3, Ausweichen 8
Riesenratte: Leben 9, Stärke 1, Verteidigung 0, Ausweichen 12
Speergoblin: Leben 16, Stärke 2, Verteidigung 1, Ausweichen 10
Skelett: Leben 28, Stärke 4, Verteidigung 2, Ausweichen 8, immun gegen Blutung und Gift
Erzgolem: Leben 46, Stärke 6, Verteidigung 4, Ausweichen 2, immun gegen Blutung und Gift
Schwäche für alle:
Barbar: Stärke 7 -> 3
Riesenratte: Stärke 1 -> 0
Speergoblin: Stärke 2 -> 0
Skelett: Stärke 4 -> 0
Erzgolem: Stärke 6 -> 2

Die Schwäche nimmt jedem 4 von der Stärke. Die Ratte, der Goblin und das Skelett fallen unter null und bleiben bei 0: Sie können mit negativer Stärke nicht zuschlagen. Der Golem hat 6, also behält er 2. Die nächste Karte sagt, warum.

Die Idee

Ein Wert ist eine Summe aus Schichten

Die Stärke des Helden ist nicht eine Zahl. Sie ist eine Summe:

  • Basis: womit der Held geboren wird (die 7 Stärke des Barbaren). Sie wird im Wesen gespeichert;
  • Ausrüstung: die Boni der Gegenstände, die gerade getragen werden (+2 vom Ring). Sie wird nicht gespeichert, sondern aus den Slots gesammelt;
  • Effekte: die Boni und Lasten der Effekte, die gerade wirken (+3 von der Raserei). Sie wird nicht gespeichert, sondern aus der Effektliste gesammelt.
// Effekte addieren sich: Zwei Effekte auf einen Wert werden zusammengezählt.
int effectBonus(String stat) {
int total = 0;
for (Effect effect : effects) total += effect.mods().getOrDefault(stat, 0);
return total;
}
// Der Rohwert (kann negativ sein): Basis + Ausrüstung + Effekte.
int rawStat(String stat) {
return base.get(stat) + gearBonus(stat) + effectBonus(stat);
}
// Ein Wert fällt nie unter null.
int stat(String stat) {
int value = rawStat(stat);
return value < 0 ? 0 : value;
}

Die Summe wird bei jeder Anfrage berechnet und nirgends gespeichert. So kann sie nicht aus dem Takt geraten. Du nimmst das Hemd ab, und die Verteidigung sinkt, weil gearBonus den Gegenstand nicht findet. Ein Effekt läuft aus, und die Stärke kommt zurück, weil effectBonus den Effekt nicht findet. Nichts „setzt die Zahl zurück“. Würde das Wesen die gesamte Verteidigung speichern, müsstest du daran denken, sie bei jedem Ereignis zu ändern: anlegen, ablegen, ein Effekt beginnt, ein Effekt endet, der Held steigt eine Stufe auf. Eine vergessene Stelle, und die Verteidigung „dreht durch“.

Der Held im Programm ist ein Barbar (48 Leben, 7 Stärke, 4 Geschick, 3 Verteidigung) mit Kettenhemd, Ring der Stärke und Raserei. Jede Zeile ist Basis +Ausrüstung +Effekte = Summe:

void addEffect(String id) {
for (Effect effect : effects) {
if (effect.id().equals(id)) return;
}
effects.add(Data.EFFECTS.get(id));
}
// Effekte addieren sich: Zwei Effekte auf einen Wert werden zusammengezählt.
int effectBonus(String stat) {
int total = 0;
for (Effect effect : effects) total += effect.mods().getOrDefault(stat, 0);
return total;
}
// Der Rohwert (kann negativ sein): Basis + Ausrüstung + Effekte.
int rawStat(String stat) {
return base.get(stat) + gearBonus(stat) + effectBonus(stat);
}
// Ein Wert fällt nie unter null.
int stat(String stat) {
int value = rawStat(stat);
return value < 0 ? 0 : value;
}
@Override
int gearBonus(String stat) {
int total = 0;
for (String slot : Data.SLOTS) {
String id = equipment.get(slot);
if (id.isEmpty()) continue;
total += Data.ITEMS.get(id).mods().getOrDefault(stat, 0);
}
return total;
}
// Das Leben übersteigt nie das neue Maximum.
void equip(String id) {
equipment.put(Data.ITEMS.get(id).slot(), id);
if (hp > maxHp()) hp = maxHp();
}
Schritt für Schritt, in Worten
  1. Gespeichert ist nur die Basis: Stärke 7, Geschick 4, Verteidigung 3. Ausweichen ist abgeleitet: Geschick × 2 = 8.
  2. Kettenhemd: Verteidigung 3 + 4 = 7. Der Ausweich-Bonus ist -5, stat() aber 0; die Rohzahl bleibt, das Ausweichen sinkt auf 3.
  3. Der Ring sitzt in einem anderen Slot und verdrängt das Hemd nicht: Slots werden summiert. Stärke 7 + 2 = 9.
  4. Raserei: +3 Stärke, −2 Verteidigung. effectBonus zählt die Effekte zusammen: Stärke 12, Verteidigung 5. Endet sie, verschwindet die Schicht.
  5. Die Summe wird neu berechnet: stat() = max(0, Basis + Ausrüstung + Effekte). Sie wird nicht gespeichert und kann nicht aus dem Takt geraten.
Hero hero = new Hero("Barbar", 48, 7, 4, 3);
System.out.println("-- Zu Beginn --");
report(hero);
hero.equip("chain");
hero.equip("ring_strength");
System.out.println("-- Mit Kettenhemd und dem Ring der Stärke --");
report(hero);
hero.addEffect("rage");
System.out.println("-- Und mit Raserei --");
report(hero);
Ausgabe
-- Zu Beginn --
Barbar: Leben 48/48
maxHp: 48 +0 +0 = 48
strength: 7 +0 +0 = 7
agility: 4 +0 +0 = 4
defense: 3 +0 +0 = 3
evasion: 0 +0 +0 = 0
Ausweichen: 8
-- Mit Kettenhemd und dem Ring der Stärke --
Barbar: Leben 48/48
maxHp: 48 +0 +0 = 48
strength: 7 +2 +0 = 9
agility: 4 +0 +0 = 4
defense: 3 +4 +0 = 7
evasion: 0 -5 +0 = 0
Ausweichen: 3
-- Und mit Raserei --
Barbar: Leben 48/48
maxHp: 48 +0 +0 = 48
strength: 7 +2 +3 = 12
agility: 4 +0 +0 = 4
defense: 3 +4 -2 = 5
evasion: 0 -5 +0 = 0
Ausweichen: 3

Sieh dir die letzte Zeile an: evasion: 0 -5 +0 = 0, aber das Ausweichen ist 3. Warum, sagt die nächste Karte.

Die Idee

Die Rohzahl und der Wert

Das Hemd ist schwer: Es nimmt 5 vom Ausweichen. Aber ein Wert mit Minuszeichen ergibt keinen Sinn: Stärke −3 würde einen Treffer in Heilung verwandeln. Deshalb fällt stat nicht unter null:

// Ein Wert fällt nie unter null.
int stat(String stat) {
int value = rawStat(stat);
return value < 0 ? 0 : value;
}
int maxHp() {
int value = stat("maxHp");
return value < 1 ? 1 : value;
}

Das Ausweichen muss aber wissen, dass das Hemd etwas wiegt. Es nutzt nicht stat, sondern die Rohzahl (rawStat), die negativ sein darf:

// Ausweichen kommt aus dem Geschick und dem rohen Bonus (der negativ sein darf).
int evasion() {
return stat("agility") * 2 + rawStat("evasion");
}

Mit Geschick 4 ist das Ausweichen 4 × 2 = 8. Das Hemd nimmt 5, aber stat("evasion") ist 0, während das Ausweichen 8 − 5 = 3 ist. Würde das Ausweichen stat("evasion") nutzen, kostete das Hemd nichts.

maxHp hat eine eigene Regel: Es fällt nicht unter 1. Ein Wesen mit 0 maximalem Leben ist tot, sobald es geboren wird, und eine Division durch das maximale Leben (zum Beispiel für einen Balken in der Oberfläche) ginge schief.

return value < 0 ?  : value;

Das Spiel macht dasselbe, mit ein paar weiteren abgeleiteten Zahlen: Treffsicherheit, Chance auf einen kritischen Treffer und „schwer verwundet“ (unter 25 % Leben sind die Treffer ungenauer):

// Der Rohwert (er kann negativ sein): Basis + Ausrüstung + Effekte.
int rawStat(String stat) {
int value = this.base.get(stat) + this.gearBonus(stat);
for (Effect effect : this.effects) value += effect.modifier(stat);
return value;
}
int stat(String stat) {
int value = this.rawStat(stat);
return value < 0 ? 0 : value;
}
int maxHp() {
int value = this.stat("maxHp");
return value < 1 ? 1 : value;
}
int maxMana() {
return this.stat("maxMana");
}
// Unter 25 % Leben ist die Wunde schwer: Treffer werden ungenauer.
boolean isHeavilyWounded() {
return this.hp * 4 < this.maxHp();
}
int accuracy() {
return 75 + this.stat("agility") * 2 + this.rawStat("accuracy") - (this.isHeavilyWounded() ? 15 : 0);
}
int evasion() {
return this.stat("agility") * 2 + this.rawStat("evasion");
}
int critChance() {
return 5 + this.stat("agility") / 10;
}

Daher kommt rawStat im Canon: Es addiert die Basis, gearBonus und jeden Effekt mit effect.modifier(stat). Die Lektion hält effectBonus getrennt, damit du die drei Schichten einzeln siehst.

Die Idee

Der Held trägt, der Feind nicht

Das Basis-Creature.gearBonus gibt 0 zurück, weil ein Feind keine Ausrüstung hat. Der Held ersetzt es:

@Override
int gearBonus(String stat) {
int total = 0;
for (String slot : Data.SLOTS) {
String id = equipment.get(slot);
if (id.isEmpty()) continue;
total += Data.ITEMS.get(id).mods().getOrDefault(stat, 0);
}
return total;
}

Der Held speichert nur, welcher Gegenstand in welchem Slot ist (Name → id). Die Zahlen selbst stehen in der Tabelle der Gegenstände. Ein Kettenhemd ist also eine Zeile Daten, keine Kopie in jedem Helden, und das Balancing zu ändern heißt, an einer Stelle eine Zahl zu ändern.

Wenn ein Gegenstand an- oder abgelegt wird, kann sich das maximale Leben ändern. Der Ring des Lebens gibt +10. Legst du ihn ab, während der Held bei 58 von 58 ist, bleibt das aktuelle Leben 58 bei einem Maximum von 48: etwas, das nicht passieren darf. Deshalb holen equip und unequip das Leben in die Grenze zurück:

// Das Leben übersteigt nie das neue Maximum.
void equip(String id) {
equipment.put(Data.ITEMS.get(id).slot(), id);
if (hp > maxHp()) hp = maxHp();
}

Im Canon ist es dasselbe (gearBonus geht die Slots durch und summiert die Modifikatoren):

@Override
int gearBonus(String stat) {
int total = 0;
for (String slot : this.equipment.keySet()) {
String itemId = this.equipment.get(slot);
if (itemId.equals("")) continue;
for (Mod mod : ITEMS.get(itemId).mods) {
if (mod.stat.equals(stat)) total += mod.value;
}
}
return total;
}
Item weapon() {
String id = this.equipment.get("weapon");
return ITEMS.get(id.equals("") ? "fists" : id);
}
Genauer, für Neugierige

Im Canon hat der Held auch einen dritten Slot, die Waffe, und weapon() gibt Fäuste zurück, wenn der Slot leer ist, damit es keinen Sonderfall „keine Waffe“ gibt. So hat der Held immer etwas zum Zuschlagen. In der Lektion gibt es zwei Slots, damit die Zahlen sichtbar bleiben.

Gegenstände werden über die id (einen String) gehalten, nicht über eine Referenz. Deshalb ist der Spielstand einfach (er speichert „ring_life“, kein Objekt), und derselbe Gegenstand kann an vielen Stellen vorkommen.

Probier es aus

Sage die Ausgabe voraus

Der Held legt den Ring des Lebens an (+10 maximales Leben) und heilt sich vollständig. Dann trifft ihn Schwäche (−4 Stärke), und er legt den Ring ab. Was gibt das Programm aus?

Hero hero = new Hero("Barbar", 48, 7, 4, 3);
hero.equip("ring_life");
hero.hp = hero.maxHp();
hero.addEffect("weak");
hero.unequip("ring");
System.out.println(hero.hp + "/" + hero.maxHp());
System.out.println(hero.stat("strength"));
Antwort

Probier es aus

Finde den Fehler

Ein Kollege hat eine Zeile „vereinfacht“, und jetzt hat ein Held mit Raserei und Schild seltsame Zahlen. Die beiden Effekte werden nacheinander gesetzt, und das Programm gibt dies aus:

Hero hero = new Hero("Barbar", 48, 7, 4, 3);
hero.addEffect("rage");
hero.addEffect("shield");
report(hero);
Ausgabe
Barbar: Leben 48/48
maxHp: 48 +0 +0 = 48
strength: 7 +0 +0 = 7
agility: 4 +0 +0 = 4
defense: 3 +0 +3 = 6
evasion: 0 +0 +0 = 0
Ausweichen: 8

Die Raserei hilft der Stärke nicht (es gibt kein +3), und die Verteidigung ist 3 + 3, nicht 3 − 2 + 3. Hier sind die drei Methoden, die die Schichten berechnen. Welche Zeile ist schuld?

Wähle die Zeile mit dem Fehler.

Genauer

Wann eine Klasse, wann eine Zeile

Wenn im Spiel etwas Neues auftaucht, frage: Unterscheidet es sich in Zahlen oder im Verhalten?

  • Ein neuer Feind mit anderem Leben, anderer Stärke und Verteidigung, einer Immunität gegen Feuer, einer anderen Stimmung: eine Zeile in der Tabelle;
  • ein neuer Gegenstand mit Modifikatoren: eine Zeile in der Tabelle, und eine neue Art von Slot ist ein neuer Name in der Liste der Slots;
  • ein Effekt, der nur Zahlen ändert (Raserei, Schild, Schwäche): eine Zeile in der Tabelle;
  • ein Effekt, der jeden Zug etwas tut (Gift, Brennen, Heilung): eine Methode, deshalb ist er im Canon eine kleine Klasse;
  • ein Feind, der entscheidet, wie er sich bewegt (der Schamane beschwört, der Wolf flieht): Verhalten, also Code. Im Canon sind das Fähigkeiten und Zustandsautomaten, keine Unterklassen von Creature.

Eine Klasse (oder eine ersetzte Methode) braucht man, wenn das andere Verhalten nicht in eine Zahl passt. Alles andere wächst mit Zeilen in einer Tabelle.

Übung

Das Amulett

Löse die Aufgabe in deinem Editor.

Amulett. Füge einen dritten Slot amulet und einen Gegenstand amulet_bear („Bärenamulett“: +6 maximales Leben, +1 Stärke, −1 Geschick) nur als neue Zeile in der Gegenstandstabelle und neuen Namen in der Liste der Slots hinzu. Keine neue Klasse. Das Programm liest Befehle aus der Eingabe (einen pro Zeile): equip <Gegenstand>, remove <Slot>, buff <Effekt> und heal (das Leben wird zum Maximum). Nach jedem Befehl schreibt es > Befehl und eine Zeile: Leben/Maximum | Stärke | Verteidigung | Geschick | Ausweichen. Der Held ist derselbe Barbar (48, 7, 4, 3).

Mit der Eingabe equip chain / equip amulet_bear / heal / buff rage / remove amulet / remove armor (je eine pro Zeile) ist die Ausgabe:

Ausgabe
> equip chain
48/48 | Stärke 7 | Verteidigung 7 | Geschick 4 | Ausweichen 3
> equip amulet_bear
48/54 | Stärke 8 | Verteidigung 7 | Geschick 3 | Ausweichen 1
> heal
54/54 | Stärke 8 | Verteidigung 7 | Geschick 3 | Ausweichen 1
> buff rage
54/54 | Stärke 11 | Verteidigung 5 | Geschick 3 | Ausweichen 1
> remove amulet
48/48 | Stärke 10 | Verteidigung 5 | Geschick 4 | Ausweichen 3
> remove armor
48/48 | Stärke 10 | Verteidigung 1 | Geschick 4 | Ausweichen 8

Achte auf heal und auf remove amulet: Mit dem Amulett ist das Maximum 54 und heal füllt bis 54 auf, während remove amulet das Leben in die Grenze von 48 zurückholt.

Weitere Aufgaben

  • Ein neuer Feind ohne Code. Füge den Blutwolf aus dem Canon (20 Leben, 3 Stärke, 8 Geschick, 1 Verteidigung, keine Immunität) nur als Zeile in die Feind-Tabelle ein. Wie hoch ist sein Ausweichen?
  • Ein zweiter Ring. Mache die Slots ring1 und ring2. Was passiert mit ring_strength, wenn du ihn in beide legst? Ist das richtig?
  • Ein Effekt mit Dauer. Gib dem Effekt eine Zahl von Zügen und eine Methode tick(), die sie verringert und den Effekt bei null entfernt. Was muss stat() nach dem Auslaufen tun?

Test

Prüfe dich

1. Im Spiel kommt ein neuer Feind dazu: wie der Goblin, aber mit 30 statt 16 Leben. Was tust du?
2. Ein Held mit 3 Verteidigung zieht ein Kettenhemd an (+4 Verteidigung) und bekommt dann Raserei (−2 Verteidigung). Was ist stat("defense")?
3. Der Held hat Geschick 4 und ein Kettenhemd mit einem Ausweich-Bonus von −5. Was ist evasion()?
4. Warum hat Creature kein Feld „Verteidigung mit Ausrüstung“, das beim Anlegen aktualisiert wird?
5. Warum überschreibt Hero.gearBonus die Methode von Creature, statt einfach ein eigenes Feld zu haben?

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