Skip to content

Instantly share code, notes, and snippets.

@gandaro
Forked from sma/ta1.mdown
Created April 15, 2012 10:09
Show Gist options
  • Save gandaro/2391688 to your computer and use it in GitHub Desktop.
Save gandaro/2391688 to your computer and use it in GitHub Desktop.
Erster Teil eines Tutorial, das ein Textadventure in Python entwickelt

Ein Textadventure (auch Interactive Fiction genannt) ist eine Spielegattung aus den frühen 80er Jahren, in der der Computer Schauplätze, Gegenstände und Personen per Text beschreibt und der Spieler einfache Befehle wie "go north" oder "take the brass lantern" gibt, um mit dem Spiel zu interagieren und letztlich interaktiv eine Geschichte erlebt, in der er Rätsel lösen muss oder überleben oder was auch immer.

Diese wenig kreative Geschichte soll als Beispiel dienen: Wir stehen vor einer alten trutzigen Burg mit hohen Mauern und tiefem Graben und haben ein Seil und Brot dabei. Von dort können wir über eine Zugbrücke in den verfallenen Burghof gehen. Vögel flattern auf und wir treffen Hugo, der bewaffnet mit einem Schwert, nicht wagt, in die Kellergewölbe zu gehen. Wir können den Burgfried besteigen und finden dort Ratten, die ein Nest mit einer Kiste mit Silbermünzen bewachten. Mit dem Brot können wir die Ratten weglocken und die Münzen nehmen. (Alternativ können wir unser Seil nehmen, um das Dach des Turms zu erreichen, von wo aus wir direkt an die Münzen kommen, allerdings ist nun das Seil weg.) Andernfalls greifen sie an, was beim dritten Biss zu unserem Tod führt. Es heißt also fliehen. Die Ratten werden nicht in den Keller folgen. Die Münzen können wir Hugo geben und sein Schwert bekommen, mit dem wir in den Keller steigen können und dort gegen einen Kobold kämpfen, der eine Tür bewacht. Ohne Schwert wird er angreifen und uns töten, wenn wir nicht sofort weglaufen. Der Kobold hat einen Schlüssel dabei, den wir nur nehmen können, wenn er besiegt ist und damit die Tür aufschließen, die über ein Loch im Boden zu einem Tempelraum führt, den wir mit unserem Seil erreichen können (sonst eben springen). Dort ist ein blutiger Altar, auf dem ein goldener sechszackiger Stern liegt. Nehmen wir den Stern (er ist wertvoll), haben wir in der PG13-Version gewonnen. Andernfalls reißt der Boden auf und eine dämonische Monstrosität erscheint, die nicht besiegt werden kann und uns tötet, wenn wir nicht fliehen. (Das können wir nur, wenn wir das Seil benutzt haben.) Der Dämon wird Hugo töten, der ruft, "lasst das Wesen nicht entkommen" und nun kann man die Zugbrücke hochkurbeln, wodurch das Wesen eingesperrt wird, allerdings mit uns zusammen...

Das ist erst einmal eine ganze Menge.

Grundsätzlich gibt es Schauplätze (gerne auch Räume genannt) wie den Burghof oder der Tempelraum, Gegenstände wie die Kiste mit Münzen oder das Schwert und Personen (oder Kreaturen) wie Hugo oder die Ratten. Räume sind miteinander verbunden und wir als Spielende befinden uns immer in einem Raum. Gegenstände sind in einem Raum, in einem anderen Gegenstand (die Münzen in der Kiste) oder "in" einer Person. Wir können einen Raum wechseln, Gegenstände nehmen, weglegen oder benutzen und mit Personen interagieren, indem wir sie ansprechen, angreifen oder ihnen etwas geben.

Wir können das Spiel auf zwei Arten in Python implementieren: Entweder direkt oder als Rahmenwerk, mit derartige Spiele über eine spezielle (sogenannte Domänen-spezifische Sprache, englisch "DSL") beschrieben werden können und dann von unserem Python-Programm interpretiert werden.

Um mehr über die "Domäne" zu lernen, wollen wir zunächst den einfacheren direkten Wert wählen.

Beginnen wir mit einer Klasse Room (ich werde das Programm englisch halten), mit deren Exemplaren wir die Schauplätze beschreiben können. Jeder Schauplatz hat einen Namen, eine Beschreibung und Verbindungen zu weiteren Räumen, die wir mit Himmelsrichtungen (oder "nach oben" oder "nach unten") beschreiben wollen.

class Room:
    def __init__(self, name, description):
        self.name = name
        self.description = description
        self.exits = {}
    
# define all rooms
vor_der_burg = Room("Vor der Burg",
    "Vor dir ragt eine alte trutzige Burg auf, das Ziel deiner tagelangen "
    "Reise durch die Wälder von Mordag. Im Norden führt eine herabgelassene "
    "Zugbrücke zu einem düsteren Burghof. Es ist still hier. Sehr still.")
burghof = Room("Der Burghof",
    "Die hohen Mauern lassen nur weg Licht der untergehenden Sonne hinein. "
    "Der Hof ist unübersichtlich. Im Laufe der Jahrhunderte hat sich hier "
    "viel Schutt und Müll angesammelt. Im Westen führt ein Tor in den "
    "Burgfried, alle anderen Zugänge scheinen verschüttet zu sein.")
burgfried = Room("Der Burgfried",
    "Hier ist es richtig dunkel. Wie praktisch wäre jetzt ein Licht. Nach "
    "einigen Treppen kommt ein kleiner Raum, in dem es entsetzlich stinkt.")

# connect rooms
vor_der_burg.exits["norden"] = burghof
burghof.exits["süden"] = vor_der_burg
burghof.exits["westen"] = burgfried
burgfried.exits["osten"] = burghof

Für die erste Version des Spiels müssen wir das Konzept eines aktuellen Raums (room) haben und dann einen Befehl wie gehe nach norden einlesen und verarbeiten können. Allgemein müssen wir mit Verben umgehen können, denen Objekte und optional noch Präpositionen folgen können. Dies soll eine Klasse namens Game regeln:

class Game:
    def play(self):
        self.enter_room(vor_der_burg)
        while self.execute_command(): pass
    
    def execute_command(self):
        words = [word.lower() for word in input("? ").strip().split() if word]
        if words:
            if words[0] in ("gehe", "geh"):
                if len(words) > 2 and words[1] == "nach":
                    self.execute_go(words[2])
                elif len(words) > 1:
                    self.execute_go(words[1])
                else:
                    emit("Wohin soll ich gehen?")
            else:
                emit("Ich verstehe '%s' nicht." % " ".join(words))
        return True
    
    def execute_go(self, direction):
        room = self.room.exits.get(direction)
        if room:
            self.enter_room(room)
        else:
            emit("Du kannst nicht nach '", direction, "' gehen.")
    
    def enter_room(self, room):
        self.room = room
        self.describe_room()
    
    def describe_room(self):
        emit()
        emit(self.room.name)
        emit()
        emit(self.room.description)
        emit()

In der ersten Zeile von execute_command lese ich eine Zeile vom Benutzer ein, entferne unnötige Leerzeichen, zerteile dann die Zeile in Wörter, die ich alle in Kleinbuchstaben verwandle -- alles in einer Zeile. Wenn jetzt etwas eingegeben wurde, prüfe ich, ob es mit gehe oder geh (was grammatikalisch wohl korrekter ist) beginnt und rufe dann execute_go auf, welches den "gehen"-Befehl implementiert. Ja, Fehleingaben können diese einfachen Tests fehlschlagen lassen. Ich überlasse es dem Leser, das zu erweitern.

In execute_go prüfe ich dann, ob es vom aktuellen Raum aus eine Verbindung in die angegebene Richtung gibt. Falls ja, werden wir diesen Raum betreten. Andernfalls wird ein Fehler angezeigt.

Wenn ein Raum betreten wird, geben wir den Namen und die Beschreibung aus.

Die Funktion emit funktioniert wie print, bricht allerdings an Wortgrenzen um, damit die Ausgabe besser aussieht. Ich gehe einfach davon aus, dass die Konsole 80 Zeichen breit ist. Theoretisch könnte man das versuchen zu erfragen:

def emit(*args, width=80):
    column = 0
    for word in "".join(str(arg) for arg in args).split():
        column += len(word) + 1
        if column > width:
            column = len(word) + 1
            print()
        print(word, end=" ")
    print()

Wir können nun das Spiel das erste Mal ausprobieren und den Burghof oder den Burgfried betreten. Wer auch die anderen Räume angelegt und verbunden hat, kann natürlich auch diese erforschen.

Weil man sich so häufig zwischen Räumen bewegt, soll es möglich sein, das Verb wegzulassen und einfach nur die Richtung anzugeben. Die folgende kleine Erweiterung macht dies möglich:

def execute_command(self):
    words = [word.lower() for word in input("? ").strip().split() if word]
    if words:
        if words[0] in ("gehe", "geh"):
            ...
        elif words[0] in self.directions:
            self.execute_go(words[0])
    ...
    
directions = ("norden", "osten", "süden", "westen", "oben", "unten")

Im englischen könnte man sogar einbuchstabige Befehle benutzen, doch auf deutsch ist "o" für "oben" oder "osten" leider eindeutig. Wer mag, kann ja noch einbauen, dass man einen Punkt am Ende eines Satzes machen kann oder auch ein "bitte" einstreuen kann. Der Computer freut sich bestimmt über ein bisschen Höflichkeit.

Und wo wir bei Höflichkeit sind. Das Programm mit ^C zu beenden, ist auch nicht so schön. Ich möchte daher "ende" als Befehl zum Beenden des Programms einführen. Man könnte hier dann z.B. den Spielstand speichern. Ich hatte in play extra eine Schleife eingebaut, die beendet wird, wenn execute_command etwas anderes als True liefert:

def execute_command(self):
    ...
        elif words[0] == "ende":
            return False

Und da ich schon alt und vergesslich bin, möchte ich mir auf Anforderung die Beschreibung eines Raums anzeigen lassen können:

def execute_command(self):
    ...
        elif words[0] in ("schaue", "schau", "beschreibung"):
            self.describe_room()

Das soll für heute erst einmal reichen.

Stefan

#!/usr/bin/env python3
class Room:
def __init__(self, name, description):
self.name = name
self.description = description
self.exits = {}
# define all rooms
vor_der_burg = Room("Vor der Burg",
"Vor dir ragt eine alte trutzige Burg auf, das Ziel deiner tagelangen "
"Reise durch die Wälder von Mordag. Im Norden führt eine herabgelassene "
"Zugbrücke zu einem düsteren Burghof. Es ist still hier. Sehr still.")
burghof = Room("Der Burghof",
"Die hohen Mauern lassen nur weg Licht der untergehenden Sonne hinein. "
"Der Hof ist unübersichtlich. Im Laufe der Jahrhunderte hat sich hier "
"viel Schutt und Müll angesammelt. Im Westen führt ein Tor in den "
"Burgfried, alle anderen Zugänge scheinen verschüttet zu sein.")
burgfried = Room("Der Burgfried",
"Hier ist es richtig dunkel. Wie praktisch wäre jetzt ein Licht. Nach "
"einigen Treppen kommt ein kleiner Raum, in dem es entsetzlich stinkt.")
# connect rooms
vor_der_burg.exits["norden"] = burghof
burghof.exits["süden"] = vor_der_burg
burghof.exits["westen"] = burgfried
burgfried.exits["osten"] = burghof
def emit(*args, width=80):
column = 0
for word in "".join(str(arg) for arg in args).split():
column += len(word) + 1
if column > width:
column = len(word) + 1
print()
print(word, end=" ")
print()
class Game:
def play(self):
self.enter_room(vor_der_burg)
while self.execute_command():
pass
def execute_command(self):
words = [word.lower() for word in input("? ").strip().split() if word]
if words:
if words[0] in ("gehe", "geh"):
if len(words) > 2 and words[1] == "nach":
self.execute_go(words[2])
elif len(words) > 1:
self.execute_go(words[1])
else:
emit("Wohin soll ich gehen?")
elif words[0] in self.directions:
self.execute_go(words[0])
elif words[0] in ("schaue", "schau", "beschreibung"):
self.describe_room()
elif words[0] == "ende":
return False
else:
emit("Ich verstehe '%s' nicht." % " ".join(words))
return True
directions = ("norden", "osten", "süden", "westen", "oben", "unten")
def execute_go(self, direction):
room = self.room.exits.get(direction)
if room:
self.enter_room(room)
else:
emit("Du kannst nicht nach '", direction, "' gehen.")
def enter_room(self, room):
self.room = room
self.describe_room()
def describe_room(self):
emit()
emit(self.room.name)
emit()
emit(self.room.description)
emit()
Game().play()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment