Text Adventure Teil 2: die Umsetzung
Nachdem wir im ersten Teil dieses Beitrags die Grundlagen eines Text-Adventure-Spiels erklärt und eine Spielidee ausgearbeitet haben, folgt im zweiten Teil nun die Umsetzung. Um das Beispiel zu bearbeiten, benötigen Sie lediglich einen Texteditor, einen Python-Interpreter und einige grundlegende Kenntnisse in der Programmiersprache Python.
Die Klasse für den Spieler
Der Protagonist des Spiels ist der Spieler. Es ist sinnvoll, hierfür ein Objekt zu erzeugen. In diesem können wir nicht nur den Namen aufnehmen, um das Text Adventure später zu individualisieren. Außerdem ist es möglich, darin abzuspeichern, ob er beziehungsweise sie das Herz eines Prinzen oder einer Prinzessin erobern will. Außerdem halten wir hier fest, an welcher Position sich der Spieler gerade befindet und welche Gegenstände er bereits gesammelt hat. Daher benötigen wir für die Klasse Spieler die folgenden Attribute:
- Name
- Ziel (Prinz oder Prinzessin?)
- Position
- Ausstattung
Dafür erstellen wir nun einen Konstruktor. Dieser benötigt jedoch keine Übergabewerte. Den Namen und das Ziel fragen wir direkt im Konstruktor ab. Die Position legen wir zu Anfang des Spiels immer mit 0 fest – also mit der Wiese, die wir als Einstiegspunkt gewählt haben. Da zu Beginn noch keine Ausstattungsgegenstände vorhanden sind, geben wir hier eine leere Liste vor. Der Konstruktor sieht demnach so aus:
Für das Ziel wollen wir festhalten, ob der Spieler das Herz einer Prinzessin oder eines Prinzen erobern will. Dafür verwenden wir genau diese beiden Begriffe. Wenn der Anwender im ersten Versuch eine andere Eingabe vornimmt, beginnen wir eine Schleife, die so lange läuft, bis er entweder genau den Begriff „Prinz“ oder „Prinzessin“ eingibt. Diese enthält außerdem eine eindeutige Anweisung.
Die Werte der Variablen Name, Ziel und Position müssen wir später aus dem Hauptprogramm abrufen. Da wir jedoch zwei Unterstriche vor den Variablennamen gestellt und die Attribute damit gekapselt haben, müssen wir hierfür noch die passenden Getter-Methoden erstellen. Danach sieht die komplette Klasse für den Spieler so aus:
Damit ist diese bereits beinahe fertiggestellt. Später müssen wir nur noch die Methode zug() hinzufügen, die den Ablauf eines Spielzugs regelt.
Die Klasse für die Orte
Im folgenden Schritt erstellen wir eine Klasse für die einzelnen Orte. Diese muss eine Beschreibung, die Anweisung für die hier mögliche Aktion und die möglichen Werte für den Ausgang enthalten. Außerdem müssen wir angeben, welchen Gegenstand der Spieler hier erwerben kann und welche Ausstattung er hierfür notwendig ist. Die entsprechenden Inhalte übergeben wir für jeden einzelnen Ort direkt bei der Erzeugung des Objekts. Daher müssen wir diese Parameter aufnehmen und sie anschließend den einzelnen Attributen zuweisen.
Da wir all diese Werte auch aus anderen Programmteilen abfragen müssen, erstellen wir dafür gleich passende Getter-Methoden. Damit ist die Klasse Ort bereits komplett abgeschlossen:
Das Hauptprogramm
Die Grundlagen für unser Programm haben wir mit den Klassen Spieler und Ort bereits erzeugt. Allerdings können wir es noch nicht ausführen, da noch kein Hauptprogramm vorhanden ist. Der nächste Schritt besteht darin, dieses zu erstellen.
Eine Einleitung schreiben und einen Spieler erzeugen
Wir beginnen damit, eine kleine Einleitung zu schreiben, die den Spieler zum Abenteuer hinführt. Dafür ist in erster Linie etwas Kreativität bei der Texterstellung notwendig. Um nicht viele einzelne print()-Methoden aufzurufen und um keine endlosen Codezeilen zu erzeugen, speichern wir diese Einleitung in einer Variable ab und geben diese anschließend aus:
Der nächste Schritt besteht dann darin, ein Objekt für den Spieler zu erzeugen. Dazu müssen wir einfach eine entsprechende Variable erstellen und die Klasse aufrufen:
spieler = Spieler()
Nun ist es bereits möglich, das Programm auszuführen. Dabei erscheint nicht nur die eben erstellte Einleitung. Da wir ein Objekt für den Spieler erstellt haben, wird der Konstruktor dieser Klasse aufgerufen. Das hat zur Folge, dass die hier festgelegten Aktionen durchgeführt werden – also die Abfrage des Namens und des Ziels des Spiels. Das zeigt der folgende Screenshot:
Sie können nun auch einmal ausprobieren, was passiert, wenn Sie bei der Abfrage des Ziels nicht gleich den richtigen Begriff eingeben. Dann erscheint eine Schleife, die so lange fortgeführt wird, bis Sie ein passendes Wort verwenden.
Die Orte erzeugen
Im nächsten Schritt erzeugen wir die Orte, die der Spieler aufsuchen kann. Im ersten Teil dieses Beitrags haben wir bereits die einzelnen Orte mit ihren jeweiligen Eigenschaften festgelegt. Daher ist es sinnvoll, sich diese Aufzeichnungen nun nochmals anzuschauen. Anhand dieser Werte erzeugen wir daraufhin für jeden einzelnen Ort ein Objekt. Dieses fügen wir dann in eine Liste ein, die schließlich alle Orte enthalten soll.
Um die hierfür notwendige Vorgehensweise vorzustellen, wird diese nun anhand des ersten Orts ausführlich dargestellt. Dafür gestalten wir zunächst eine Beschreibung. Dafür ist es wichtig, einen interessanten Text zu erstellen, der den Spieler in die richtige Stimmung versetzt. Da dieser etwas länger ausfallen kann, ist es sinnvoll, ihn zunächst in einer separaten Variable zu speichern. Um das Spiel ansprechender zu gestalten, ist es sinnvoll, hierbei eine persönliche Ansprache einzubauen und den Namen des Spielers zu verwenden. Da wir diesen bereits abgefragt haben, sollte das kein Problem darstellen. Die Beschreibung für den ersten Ort sieht dann so aus:
Nun müssen wir die Aktionen festhalten, die der Spieler hier durchführen kann. Dazu geben wir die wörtlichen Befehle vor, mit denen er diese durchführen kann. Am ersten Ort wäre dies “Schaue in den Brunnen”. In diesem Fall spielt es keine Rolle, ob unser Spieler eine Prinzessin oder einen Prinzen finden will. In späteren Situationen können hierfür jedoch jeweils unterschiedliche Befehle notwendig sein. Um eine einheitliche Verarbeitung zu ermöglichen, ist es sinnvoll, bereits jetzt die Struktur darauf auszurichten. Daher erstellen wir eine Liste, die an erster Stelle den Befehl für die Prinzessin und an zweiter Stelle den Befehl für den Prinzen enthält. Da diese am ersten Ort jedoch identisch sind, geben wir zweimal den gleichen Befehl ein:
befehle = ["Schaue in den Brunnen", "Schaue in den Brunnen"]
Nun können wir bereits das entsprechende Objekt erzeugen. Dabei fügen wir es gleich in eine Liste ein:
orteListe = [Ort(beschreibung, befehle, [2, 1, -1, -1], "Schlüssel", "")]
Die ersten beiden Werte sollten hierbei klar sein – schließlich haben wir diese soeben erstellt. Der dritte Wert bedarf jedoch noch einer weiteren Erklärung. Hierbei geben wir die Orte an, die der Spieler aufsuchen kann, wenn er sich an der entsprechenden Position befindet – also die Ausgänge. Dabei geben wir immer eine Liste mit vier Elementen an. Diese geben die Orte in den vier Himmelsrichtungen an. Dabei müssen wir stets eine genaue Reihenfolge einhalten: Norden, Osten, Süden, Westen. Die Liste enthält die Nummer des Orts an, den der Spieler aufsucht, wenn er sich in die entsprechende Himmelsrichtung bewegt. Sollte sich dort kein weiterer Ort auf unserem Spielfeld befinden, geben wir den Wert -1 an. Wenn wir unsere Skizze nochmals betrachten, stellen wir fest, dass wir den Ort 2 aufsuchen, wenn wir uns nach Norden bewegen. Wenn wir nach Osten gehen, suchen wir hingegen den Ort 1 auf. Im Süden und im Westen ist das Spielfeld zu Ende. Das führt zu der Zahlenfolge 2, 1, -1, -1.
Der vierte Wert gibt an, welchen Gegenstand wir hier erhalten können. An Orten, an denen kein Gegenstand verfügbar ist, fügen wir hier eine leere Zeichenkette ein – also einfach zwei Anführungszeichen. Der letzte Übergabewert gibt an, welche Ausstattung für die Durchführung der Aktion notwendig ist. Da im ersten Raum kein Gegenstand notwendig ist, verwenden wir hier wieder eine leere Zeichenkette.
Nach genau dem gleichen Muster erstellen wir daraufhin auch die übrigen sechs Orte. Dabei müssen wir lediglich darauf achten, dass wir die Liste nicht mit einem Gleichheitszeichen komplett überschreiben. Stattdessen verwenden wir das Pluszeichen und das Gleichheitszeichen, um den neuen Ort zur bestehenden Liste hinzuzufügen:
Lediglich der letzte Ort sollte hierbei nochmals etwas genauer betrachtet werden. Hierbei ist eine unterschiedliche Beschreibung notwendig – je nachdem, ob ein Prinz oder eine Prinzessin erobert werden soll. Daher müssen wir den entsprechenden Wert aus dem Objekt spieler abfragen und den Text entsprechend anpassen. Außerdem geben wir zwei unterschiedliche Befehle für die Aktionen vor.
Begrüßung und Schleife für die Durchführung der Züge
Nachdem wir die Räume erzeugt haben, erstellen wir eine personalisierte Begrüßung für den Spieler mit seinem Namen. Danach fragen wir ihn, ob er bereit für das Spiel ist. Gibt er hier den Wert „ja“ ein, beginnen wir mit dem eigentlichen Zug. Dabei berücksichtigen wir auch verschiedene Alternativen hinsichtlich der Groß- und Kleinschreibung. Sollte der Spieler einen anderen Wert eingeben, verabschieden wir ihn.
Wenn der Spieler hingegen mit dem Spiel beginnen will, müssen wir alle Züge nacheinander durchführen. Daher verwenden wir eine Schleife. Diese steuern wir über Position des Spielers. Alle Orte haben einen positiven Wert. Solange sich der Spieler hier aufhält, soll das Spiel weitergehen. Wenn er jedoch das Ziel erreicht hat, soll die Schleife abgebrochen werden. Später werden wir für diesen Fall einen negativen Wert für die Position eingeben. Dieser soll zum Abbruch der Schleife führen. Deshalb geben wir als Bedingung vor, dass die Position des Spielers größer oder gleich 0 ist. Innerhalb der Schleife rufen wir lediglich die Methode zug() auf, die wir anschließend erstellen werden. Mit den folgenden Codezeilen ist unser Hauptptogramm abgeschlossen:
Die Methode zug() – das entscheidende Element für unser Spiel
Das letzte Element, das wir erstellen müssen, ist die Methode zug(), die zur Klasse Spieler gehört. Sie gibt die Beschreibung eines Orts aus, sie muss die Eingaben des Anwenders verarbeiten und die gewonnenen Gegenstände festhalten. Das zeigt bereits, dass es sich hierbei um eine etwas komplexere Aufgabe handelt. Daher ist es sinnvoll, hierbei Schritt für Schritt vorzugehen.
Beim Aufruf der entsprechenden Methode haben wir die Liste mit den Orten übergeben. Diese müssen wir nun aufnehmen. Daher sieht die erste Zeile der Methode so aus:
def zug(self, orte):
In der Methode erzeugen wir zunächst eine Liste, die die Anweisungen zum Aufsuchen eines neuen Orts enthält – für alle vier Himmelsrichtungen. Diese werden wir noch später benötigen. Danach rufen wir die Beschreibung des Orts auf, der der aktuellen Position des Spielers entspricht. Der entsprechende Code dafür sieht so aus:
richtungen = ["Gehe nach Norden", "Gehe nach Osten", "Gehe nach Süden", "Gehe nach Westen"] print (orte[self.__Position].getBeschreibung())
Im nächsten Schritt überlegen wir uns, welche Befehle der Spieler an dieser Stelle eingeben kann und welche Aktionen diese zur Folge haben. Das Ergebnis zeigt die folgende Liste:
- Der Spieler führt die Aktion aus, die an diesem Ort möglich ist.
⦁ Es muss überprüft werden, ob er die hierfür notwendige Ausstattung besitzt.
⦁ Ist die erforderliche Ausstattung nicht vorhanden, ist eine entsprechende Meldung notwendig.
⦁ Ist die Aktion erfolgreich, erhält er den entsprechenden Gegenstand.
⦁ Besitzt er diesen bereits, ist eine passende Nachricht notwendig.
⦁ Der Spieler verlässt den Ort nicht (es sei denn, er befindet sich auf dem letzten Feld und hat das Spiel mit dieser Aktion beendet). - Der Spieler sucht einen anderen Ort auf.
⦁ Ist an der entsprechenden Stelle ein Ausgang vorhanden, verlässt er den Ort.
⦁ Ist dort kein Ausgang vorhanden, erscheint eine entsprechende Meldung und er verlässt den Ort nicht. - Der Spieler gibt „Hilfe“ ein.
⦁ Es erscheinen alle Befehle, die an diesem Ort möglich sind.
⦁ Der Spieler verlässt den Ort nicht. - Der Spieler gibt „Ausstattung“ ein.
⦁ Es erscheinen alle Ausstattungsgegenstände, die er bereits gesammelt hat.
⦁ Der Spieler verlässt den Ort nicht. - Der Spieler gibt einen ungültigen Befehl ein.
⦁ Es erscheint eine passende Fehlermeldung.
⦁ Der Spieler verlässt den Raum nicht.
Diese Liste macht deutlich, dass hierbei fünf verschiedene Möglichkeiten bestehen, die wir jeweils gesondert behandeln müssen. Außerdem wird ersichtlich, dass der Spieler nur bei einer einzigen Aktion einen neuen Ort aufsucht. Bei allen anderen Befehlen bleibt er an der gleichen Stelle. Daher ist es sinnvoll, eine Schleife zu verwenden, um die Eingabe zu wiederholen. Diese erhält die Bedingung True, die immer erfüllt ist. Wenn der Spieler einen anderen Ort aufsucht, beenden wir die Schleife mit dem break-Befehl.
Damit der erste Punkt der Liste eintritt, müssen mehrere Einzel-Bedingungen erfüllt sein. Um das Programm übersichtlicher zu gestalten, ist es sinnvoll, diese zunächst in einzelnen Variablen abzuspeichern. Zunächst überprüfen wir, ob sich der Befehl in der Liste mit den Befehlen für den entsprechenden Ort befindet. Um eine Bearbeitung an Orten ohne Aktionsmöglichkeit auszuschließen, stellen wir jedoch auch sicher, dass es sich hierbei nicht um eine leere Zeichenkette handelt:
bedingung1 = anweisung != "" and anweisung in orte[self.__Position].getAnweisung()
Die zweite und die dritte Bedingung überprüfen, ob der eingegebene Befehl zum Ziel passt, entweder einen Prinzen oder eine Prinzessin zu erobern. Soll eine Prinzessin erobert werden, muss der Befehl mit der Zeichenkette an der ersten Position der Liste mit den Kommandos übereinstimmen. Ist ein Prinz das Ziel, muss der Befehl hingegen an zweiter Stelle stehen:
bedingung2 = (anweisung == orte[self.__Position].getAnweisung()[0] and self.__Ziel == "Prinzessin") bedingung3 = (anweisung == orte[self.__Position].getAnweisung()[1] and self.__Ziel == "Prinz")
Wenn die erste Bedingung und entweder die zweite oder die dritte Bedingung zutreffen, hat der Anwender einen gültigen Befehl eingegeben und wir arbeiten die entsprechende Aktion ab. In der entsprechenden if-Abfrage sind jedoch noch mehrere weitere Überprüfungen notwendig. Zunächst bringen wir in Erfahrung, ob der hier erhältliche Gegenstand nicht bereits vorhanden ist. Danach stellen wir sicher, dass der Spieler die hierfür erforderliche Ausstattung besitzt beziehungsweise dass an diesem Ort kein Gegenstand erforderlich ist. In diesem Fall fügen wir das erworbene Objekt zur Ausstattung hinzu. Allerdings müssen wir auch noch überprüfen, ob damit nicht bereits das Spiel beendet ist. Wenn sich der Spieler am Ort mit der Nummer 6 befindet, hat er mit der entsprechenden Aktion das Ziel des Spiels erreicht. Daher geben wir eine Nachricht aus und setzen die Position auf den Wert -1 um die Schleife im Hauptprogramm zu beenden. Außerdem verlassen wir die Schleife mit einem break-Befehl. Die komplette if-Abfrage für die entsprechende Aktion sieht dann so aus:
Diese Abfrage ist etwas kompliziert und es ist wichtig, die einzelnen Punkte genau zu betrachten, um den Aufbau zu verstehen. Danach wird es jedoch etwas einfacher. Nun müssen wir den Fall bearbeiten, dass der Spieler einen neuen Ort aufsuchen will. Das ist der Fall, wenn sich der eingegebene Befehl in der zuvor erstellten Liste richtungen befindet. Allerdings müssen wir danach noch überprüfen, ob an der entsprechenden Stelle ein Ausgang vorhanden ist. Trifft dies zu, suchen wir den neuen Ort auf. Ist kein Ausgang vorhanden, geben wir eine entsprechende Meldung aus:
Im nächsten Schritt bearbeiten wir den Fall, dass der Anwender Hilfe benötigt. Hierfür geben wir den Befehl für die Aktion aus, die er an diesem Ort durchführen kann. Außerdem geben wir alle Richtungen an, in die er sich bewegen kann. Dazu durchlaufen wir diese in einer for-Schleife und überprüfen jeweils, ob dort ein Ausgang vorhanden ist:
Für den Fall, dass der Spieler die Ausstattung anzeigen lassen will, müssen wir lediglich die entsprechende Liste ausgeben:
elif anweisung == "Ausstattung": print("\nDu hast folgende Gegenstände gewonnen:") for gegenstand in self.__Ausstattung: print(gegenstand)
Die letzte Option tritt dann ein, wenn der Anwender keinen gültigen Befehl eingegeben hat:
else: print("\nDiesen Befehl kenne ich nicht!")
Mit dieser Anweisung ist die Methode zug() und damit das komplette Programm abgeschlossen. Wenn Sie den Programmcode der gesamten Methode überprüfen wollen, empfiehlt es sich, den nächsten Abschnitt zu betrachten. Dieser enthält das vollständige Programm. Nun können wir dieses ausprobieren und das Abenteuer nachspielen. Der folgende Screenshot zeigt einen Ausschnitt aus dem Ablauf des Spiels.
Der komplette Programmcode
Zum Abschluss soll nun nochmals der komplette Programmcode ausgegeben werden. So können Sie diesen genau überprüfen:
Fazit: Vielfältige Möglichkeiten für spannende Abenteuer
Dieses Beispiel hat gezeigt, dass es bereits mit einigen grundlegenden Programmierkenntnissen möglich ist, ein Text-Adventure-Spiel zu gestalten. Dabei ist es lediglich notwendig, sich eine gute Struktur zu überlegen. Die Implementierung der einzelnen Schritte ist vergleichsweise einfach. Daher stellt es eine sehr gute Übung dar, ein solches Programm zu erstellen.
Das Spiel, das wir hier entworfen haben, bietet außerdem vielfältige Erweiterungsmöglichkeiten. Es stellt kein Problem dar, am Anfang des Hauptprogramms weitere Orte einzufügen. Dabei müssen Sie sich lediglich überlegen, welche Aktionen Sie hierbei durchführen wollen und wie Sie diese in das bisherige Spiel einbinden. So können Sie selbst etwas experimentieren und dabei spannende Abenteuer erleben.
Danke auch für den zweiten Teil dieses Blogs. So was in Theorie sich auszudenken ist immer eine Sache. Noch jemanden zu haben, der einem sagt, wie die Theorie in die Praxis umzusetzen ist, eine ganz andere. Ich denke, die Idee das Textadventures wird mir helfen, etwas “greifbares” in Python (vielleicht auch in Javascript?) zu erstellen. Danke für die Anleitung. Fan von Euren Büchern bin ich sowieso schon. Gerade bin ich mit Eurem Javascript Buch zugange. Toll! Python kommt als nächstes 🙂
Hallo Rudi,
Danke für Ihren netten Kommentar!
Es freut uns sehr, dass Sie unsere Blogartikel hilfreich finden und einiges daraus mitnehmen können. Wir arbeiten ständig daran, um unseren Lesern einen Mehrwert sowohl in unseren Büchern, als auch auf unserer Webseite anzubieten.
Olena vom BMU Team