Zum Inhalt
wannacode

RPG 3: das ganze Spiel

Kurz gesagt: Das ganze Rollenspiel: Eine Klasse Game führt den Helden durch Kämpfe, Rast, Stufen und Erfahrung, und ein falscher Zug wirft GameException, die das Spiel abfängt, um weiterzumachen.

Problem

Von einem Kampf zu einem Spiel

Die Helden und die Monster kämpfen schon. Aber ein Spiel ist mehr als ein Kampf: Du erschaffst einen Helden, stellst die Monster auf, gegen die er antritt, und er geht sie nacheinander durch. Zwischen den Kämpfen ruht er sich aus oder heilt sich mit Magie.

Wenn das alles in Main steht, liegen dort der Held, ein Array mit Monstern, die Nummer des nächsten, Zähler und Prüfungen. Und wer hält den Spieler davon ab, einen Kampf ohne Helden zu starten oder von einem Barbaren Magie zu verlangen?

Der Zustand des Spiels und seine Regeln müssen an einem Ort stehen: in einer Klasse Game.

Die Idee

Die Klasse Game

Der Held und die Monster sind der Zustand des Spiels. Die Methoden von Game sind die Regeln: was der Spieler tun kann und was danach passiert. Die erste ist, einen Helden zu erstellen:

private Character hero;
private Monster[] Monster = new Monster[5];
private int count;
private int next;
private Random random;
public Game(Random random) {
this.random = random;
}
public void createHero(String name, String race) {
if (race.equals("paladin")) {
hero = new Paladin(name);
} else if (race.equals("barbar")) {
hero = new Barbarian(name);
} else if (race.equals("hexe")) {
hero = new Witch(name);
} else {
throw new GameException("Dieses Volk gibt es nicht: " + race);
}
}
Der Zufallsgenerator kommt von außen, über den Konstruktor. Im echten Spiel ist es new Random(), und in den Beispielen hat er einen Startwert, damit die Ausgabe immer gleich ist.

Ein unbekanntes Volk ist kein Grund, irgendeinen Helden zu erschaffen. Die Methode wirft GameException: unsere eigene Exception für jeden Zug, den die Regeln nicht erlauben. Sie bekommt weiter unten eine eigene Karte.

Die Idee

Eine Reihe von Monstern

Die Monster warten in einer Schlange: Das zuerst hinzugefügte ist der erste Kampf. Hier ist die Schlange ein Array und zwei Indizes:

public void addMonster(Monster monster) {
if (count == Monster.length) {
throw new GameException("Die Reihe ist voll: höchstens " + Monster.length + " Monster");
}
Monster[count] = monster;
count++;
}
public Monster createRandomMonster() {
int kind = random.nextInt(3);
if (kind == 0) {
return new FallenShaman();
}
if (kind == 1) {
return new SkeletonKing();
}
return new Butcher();
}
public boolean hasMonsters() {
return next < count;
}
  • count ist die Anzahl der bisher hinzugefügten Monster. Das neue kommt nach monsters[count].
  • next ist das Monster, das als Nächstes kämpft. Nach jedem Kampf steigt es um 1, und wenn es count erreicht, ist die Reihe zu Ende.
Das Array hat 5 Plätze. Das sechste Monster kommt nicht hinein: AddMonster wirft GameException, bevor es außerhalb des Arrays schreibt.

CreateRandomMonster ist der Bonus aus der alten Übung: ein zufällig gewähltes Monster.

Genauer, für Neugierige
  • Die alte Übung hielt die Monster in einer echten Warteschlange, Queue<Monster>. Eine Warteschlange merkt sich selbst, welches das erste ist, und sie wird nie voll. Die Liste List<T> kommt im Modul Web und Desktop, und Queue<T> stammt aus derselben Bibliothek und wird genauso benutzt: Enqueue fügt hinten an, Dequeue entnimmt vorne.
  • Die Monster vor next bleiben in dem Array. Sie schaden nicht: Niemand liest sie mehr.

Die Idee

Stufen und Erfahrung

Nach jedem Sieg bekommt der Held Erfahrung in Höhe des Lebens des Monsters. Auf Stufe 1 braucht er 100 Erfahrung bis Stufe 2, auf Stufe 2 braucht er 200 bis Stufe 3 und so weiter. Eine neue Stufe gibt 10 Leben mehr, 3 Kraft mehr und volles Leben:

public boolean gainExperience(int exp) {
experience += exp;
boolean up = false;
while (experience >= level * 100) {
experience -= level * 100;
levelUp();
up = true;
}
return up;
}
public void levelUp() {
level++;
maxHealth += 10;
power += 3;
health = maxHealth;
}

Die Methoden stehen in Character. Erfahrung über der Schwelle bleibt für die nächste Stufe. Die while-Schleife ist für den Fall, dass ein Sieg für zwei Stufen reicht.

Die Felder MaxHealth, Level und Experience sind neu in Character. Mit MaxHealth hebt Heilen das Leben nicht mehr über das Maximum.

Probier es aus

Auf welcher Stufe steht der Barbar?

Grok beginnt auf Stufe 1 mit Kraft 20. Er gewinnt gegen den Schamanen (60 Erfahrung) und gegen den König (90 Erfahrung).

Was gibt das Programm aus? Stufe, Erfahrung, Kraft.

public bool GainExperience(int exp)
{
Experience += exp;
bool up = false;
while (Experience >= Level * 100)
{
Experience -= Level * 100;
LevelUp();
up = true;
}
return up;
}
public void LevelUp()
{
Level++;
MaxHealth += 10;
Power += 3;
Health = MaxHealth;
}
Character grok = new Barbarian("Grok");
grok.GainExperience(60);
grok.GainExperience(90);
Console.WriteLine(grok.Level + " " + grok.Experience + " " + grok.Power);
Antwort

Sieh es dir an

Die Schlacht und die Rast

StartBattle prüft zuerst, ob der Kampf möglich ist. Dann nimmt es das nächste Monster und lässt den Kampf auf Leben und Tod aus dem zweiten Teil laufen. Es gibt zurück, ob der Held überlebt hat:

public boolean startBattle() {
if (hero == null) {
throw new GameException("Erstelle zuerst einen Helden");
}
if (!hasMonsters()) {
throw new GameException("Keine Monster in der Reihe");
}
Monster monster = Monster[next];
next++;
int rounds = 0;
while (hero.isAlive() && monster.isAlive()) {
rounds++;
hero.attack(monster);
if (monster.isAlive()) {
monster.attack(hero);
}
}
if (!hero.isAlive()) {
System.out.println(hero.getName() + " fällt in Runde " + rounds + ". Sieger: " + monster.getName());
return false;
}
System.out.println(monster.getName() + " wird in " + rounds + " Runden besiegt. " + hero.getName() + ": Leben " + hero.getHealth() + "/" + hero.getMaxHealth());
if (hero.gainExperience(monster.getReward())) {
System.out.println(hero.getName() + " erreicht Stufe " + hero.getLevel() + ": Leben " + hero.getMaxHealth() + ", Kraft " + hero.getPower());
}
return true;
}

Recover ist die Wahl nach dem Kampf. Nur ein Held, der IHealable implementiert, kann Magie nutzen. Die Rast dauert 5 Züge zu je 10 Leben:

public void recover(String way) {
if (way.equals("ruhe")) {
for (int turn = 0; turn < 5; turn++) {
hero.restore(10);
}
System.out.println(hero.getName() + " ruht 5 Züge lang: Leben " + hero.getHealth() + "/" + hero.getMaxHealth());
} else if (way.equals("magie")) {
if (!(hero instanceof IHealable)) {
throw new GameException(hero.getName() + " beherrscht keine Heilmagie");
}
System.out.println(hero.useAbility());
} else {
throw new GameException("Unbekannte Wahl „" + way + "“: ruhe oder magie?");
}
}

In der alten Übung wartete der Held 5 Sekunden. Hier zählt die Rast in Zügen, weil in einem rundenbasierten Spiel die Zeit in Zügen gemessen wird und die Prüfung des Spiels nicht warten soll.

Sieh es dir an

Das ganze Spiel

Grok der Barbar gegen den Schamanen und zwei zufällige Monster. Nach jedem Kampf ruht er sich aus. Fällt er, ist das Spiel aus:

Game game = new Game(new Random(12));
game.createHero("Grok", "barbar");
game.addMonster(new FallenShaman());
game.addMonster(game.createRandomMonster());
game.addMonster(game.createRandomMonster());
while (game.hasMonsters()) {
if (!game.startBattle()) {
System.out.println("Spiel vorbei");
break;
}
game.recover("ruhe");
}
Ausgabe
Gefallener Schamane wird in 3 Runden besiegt. Grok: Leben 90/110
Grok ruht 5 Züge lang: Leben 110/110
Gefallener Schamane wird in 3 Runden besiegt. Grok: Leben 90/110
Grok erreicht Stufe 2: Leben 120, Kraft 23
Grok ruht 5 Züge lang: Leben 120/120
Schlächter wird in 6 Runden besiegt. Grok: Leben 70/120
Grok ruht 5 Züge lang: Leben 120/120
Vergleiche die Ausgabe in den beiden Tabs. Der Startwert ist in beiden Sprachen 12, aber die Generatoren sind verschieden, und die zufälligen Monster auch. In Java begegnet Grok einem zweiten Schamanen und dem Schlächter, in C# dem Schlächter und dem Skelettkönig.

Game game = new Game(new Random(12));
game.CreateHero("Grok", "barbar");
game.AddMonster(new FallenShaman());
game.AddMonster(game.CreateRandomMonster());
game.AddMonster(game.CreateRandomMonster());
while (game.HasMonsters())
{
if (!game.StartBattle())
{
Console.WriteLine("Spiel vorbei");
break;
}
game.Recover("ruhe");
}
Schritt für Schritt, in Worten
  1. Das Spiel bekommt einen Generator mit Startwert 12. Der Held ist Grok, und das Array hat 5 Plätze, noch leer.
  2. Jedes AddMonster() schreibt nach monsters[count] und erhöht count. Das zweite und das dritte sind zufällig: Schlächter und Skelettkönig.
  3. next ist 0: Es kommt Gefallener Schamane. Es wird in 3 Runden besiegt, und Grok hat noch 90/110.
  4. next wird 1. Die Rast sind 5 Züge zu je 10 Leben: 110/110.
  5. next ist 1: Es kommt Schlächter. Es wird in 7 Runden besiegt, und Grok hat noch 48/110. Die Erfahrung reicht für Stufe 2: Leben 120, Kraft 23.
  6. next wird 2. Die Rast sind 5 Züge zu je 10 Leben: 120/120.
  7. next ist 2: Es kommt Skelettkönig. Es wird in 5 Runden besiegt, und Grok hat noch 68/120.
  8. next wird 3. Die Rast sind 5 Züge zu je 10 Leben: 118/120.
  9. next ist 3, genauso wie count: HasMonsters() gibt false zurück, und die Schleife endet.

Die Idee

Ein falscher Zug: GameException

Der Spieler kann einen Kampf ohne Helden verlangen, ein Volk, das es nicht gibt, oder Magie von einem Barbaren. Das ist kein Fehler im Programm. Es ist ein Zug, den die Regeln nicht erlauben. Die Methoden von Game werfen dafür ihre eigene Exception:

class GameException extends RuntimeException {
public GameException(String message) {
super(message);
}
}

Das Spiel fängt nur diese eine ab, sagt dem Spieler, warum der Zug nicht erlaubt ist, und macht mit dem nächsten Zug weiter:

Game game = new Game(new Random(12));
String[] commands = { "kampf", "held Grok drache", "held Grok barbar", "magie", "schlaf" };
for (String command : commands) {
String[] words = command.split(" ");
try {
if (words[0].equals("kampf")) {
game.startBattle();
} else if (words[0].equals("held")) {
game.createHero(words[1], words[2]);
System.out.println("Der Held ist " + words[1]);
} else {
game.recover(words[0]);
}
} catch (GameException e) {
System.out.println("Nicht erlaubt: " + e.getMessage());
}
}
Ausgabe
Nicht erlaubt: Erstelle zuerst einen Helden
Nicht erlaubt: Dieses Volk gibt es nicht: drache
Der Held ist Grok
Nicht erlaubt: Grok beherrscht keine Heilmagie
Nicht erlaubt: Unbekannte Wahl „schlaf“: ruhe oder magie?

Warum eine eigene Exception und nicht die eingebaute ArgumentException? Weil catch (GameException e) nur die Ablehnungen nach den Regeln abfängt. Hat das Spiel einen Fehler und wirft NullReferenceException, versteckt sich dieser nicht als „Nicht erlaubt“. Er hält das Programm an, und du siehst ihn.

Probier es aus

Finde den Fehler

Das sechste Monster sollte „Die Reihe ist voll“ bekommen. Stattdessen bricht das Programm mit IndexOutOfRangeException ab. In welcher Zeile steckt der Fehler?

Wähle die Zeile mit dem Fehler.

Probier es aus

Was fehlt?

Von welcher Klasse erbt GameException?

class GameException : 
{
    public GameException(string message) : base(message)
    {
    }
}

Übung

Spielen und erweitern

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

1. Ein Menü. Die Befehle kommen aus der Eingabe, einer pro Zeile, bis „ende“:

  • „held Name Volk“ erstellt den Helden;
  • „monster schamane“ (oder „könig“, „schlächter“) fügt ein fertiges Monster hinzu, und „monster Name Leben Kraft“ fügt dein eigenes hinzu: eine Klasse Beast mit dem Volk „Bestie“;
  • „zufall“ fügt ein zufälliges Monster hinzu;
  • „kampf“ startet den nächsten Kampf, und „ruhe“ und „magie“ sind die Wahl danach.

Jede Ablehnung ist eine GameException, die das Menü abfängt, mit der Meldung „Nicht erlaubt: …“. Der Startwert ist 12.

Eingabe
held Morena Elf
held Morena hexe
monster Wolf 50 9
monster drache
monster schamane
zufall
kampf
magie
kampf
ruhe
kampf
ende
Ausgabe
Nicht erlaubt: Dieses Volk gibt es nicht: Elf
Der Held ist Morena
In der Reihe: Wolf
Nicht erlaubt: Dieses Monster gibt es nicht: drache
In der Reihe: Gefallener Schamane
In der Reihe: Schlächter
Wolf wird in 4 Runden besiegt. Morena: Leben 68/95
Morena ruft die Elemente an: Leben 83/95
Gefallener Schamane wird in 4 Runden besiegt. Morena: Leben 47/95
Morena erreicht Stufe 2: Leben 105, Kraft 19
Morena ruht 5 Züge lang: Leben 105/105
Schlächter wird in 7 Runden besiegt. Morena: Leben 30/105

2. Fortschritt. Füge Game eine Methode ShowProgress() hinzu, die die Stufe des Helden zeigt und wie viel Erfahrung er von der Menge hat, die er für die nächste Stufe braucht. Ardan der Paladin geht durch zwei Schamanen und den Skelettkönig. Nach jedem Kampf zeigt er seinen Fortschritt und heilt sich mit Magie.

Ausgabe
Gefallener Schamane wird in 5 Runden besiegt. Ardan: Leben 72/120
Stufe 1, Erfahrung 60/100
Ardan heilt sich: Leben 92/120
Gefallener Schamane wird in 5 Runden besiegt. Ardan: Leben 44/120
Ardan erreicht Stufe 2: Leben 130, Kraft 17
Stufe 2, Erfahrung 20/200
Ardan heilt sich: Leben 130/130
Skelettkönig wird in 8 Runden besiegt. Ardan: Leben 18/130
Stufe 2, Erfahrung 110/200
Ardan heilt sich: Leben 38/130

3. Ein Boss. Eine Klasse Diablo, eine Unterklasse von Monster: 160 Leben, 14 Kraft. Jeder dritte Treffer von ihm zählt doppelt. Eine Methode CanChallenge(Character hero) sagt, ob der Held mindestens auf Stufe 2 ist. Wenn nicht, wirft StartBattle GameException, und Diablo bleibt in der Reihe. Lass ihn einmal gegen Grok auf Stufe 1 laufen, dann in einem neuen Spiel nach dem Schamanen und dem König.

Ausgabe
Nicht erlaubt: Diablo kämpft nur gegen Helden ab Stufe 2
Gefallener Schamane wird in 3 Runden besiegt. Grok: Leben 90/110
Grok ruht 5 Züge lang: Leben 110/110
Skelettkönig wird in 6 Runden besiegt. Grok: Leben 45/110
Grok erreicht Stufe 2: Leben 120, Kraft 23
Grok ruht 5 Züge lang: Leben 120/120
Diablo wird in 7 Runden besiegt. Grok: Leben 24/120
Grok erreicht Stufe 3: Leben 130, Kraft 26
Grok ruht 5 Züge lang: Leben 130/130

Mach allein weiter

Die alte Übung hatte weitere Ideen. Manche brauchen ein Werkzeug, das erst später kommt:

  • Fertigkeiten mit Stufen. Eine Klasse Skill mit einem Namen und einer Stufe, UseSkill(Character target) und UpgradeSkill(). Das kannst du schon jetzt, mit einem Array von Fertigkeiten im Helden.
  • Inventar. Eine Klasse Item mit einem Namen und einer Wirkung, ApplyEffect(Character target), und ein Inventory, in dem Gegenstände hinzugefügt und entfernt werden. Mit einem Array ist das umständlich. Die Liste List<T> kommt im Modul Web und Desktop.
  • Bosse mit Voraussetzungen. Ein Boss, der bestimmte Gegenstände und Fertigkeiten verlangt, bevor er den Kampf annimmt, und nach einem Sieg Beute fallen lässt. Dafür brauchst du das Inventar von oben.
  • Bildschirme. Menü, Karte, Inventar: Jeder Bildschirm ist ein Objekt mit Show(), Hide() und Update(), und die offenen Bildschirme liegen auf einem Stapel, damit „zurück“ funktioniert. In C# ist der Stapel Stack<T>, aus derselben Bibliothek wie List<T> aus dem Modul Web und Desktop.
  • Speichern und Laden. Der Held, die Stufe und das beste Ergebnis werden in eine Datei geschrieben und beim nächsten Start gelesen. Dieser Kurs hat keine Lektion über Dateien. Der kürzeste Weg sind File.WriteAllText und File.ReadAllText aus System.IO.

Test

Teste dich

1. Die Reihe ist ein Array mit 5 Plätzen, count ist 3, next ist 1. Was bedeutet das?
2. Der Held ist auf Stufe 1 mit 80 Erfahrung und gewinnt 130 Erfahrung. Was passiert?
3. Warum fängt das Spiel nur GameException ab und nicht jede Exception?
4. Warum dauert die Rast 5 Züge und nicht 5 Sekunden Warten?
5. Was fängt catch (GameException e) ab?

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