Skip to content

Instantly share code, notes, and snippets.

@sma
Created April 15, 2012 09:58
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save sma/2391632 to your computer and use it in GitHub Desktop.
Save sma/2391632 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 (oder die Spielerin) einfache Befehle wie "go north" oder "take the brass latern" gibt, um mit dem Spiel zu interagieren und letztlich interaktiv eine Geschichte erlebt, in der er (oder sie) 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 bewachen, in der eine Kiste mit Silbermünzen liegt. 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 die Ratten an, was beim dritten Biss zu unserem Tod führt. Es heißt also fliehen. Die Ratten werden nicht in den Burghof folgen. Die Münzen können wir Hugo geben und sein Schwert bekommen, mit dem wir in den Kellergewölbe steigen können und dort gegen einen Kobold kämpfen, der eine Falltü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 zu einem Tempelraum führt, den wir mit unserem Seil erreichen können (sonst müssen wir 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 nun 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 noch haben. Der Dämon wird Hugo töten, der noch 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 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, der Kobold 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. Personen sind (wie wir) immer in einem Raum. 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 Weg 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 "oben" / "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. Doch 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 "
    "einige Treppen kommt ein kleiner Raum, in dem es entsetzlich stinkt. "
    "Wäre die restliche Treppe nicht eingestürzt, ginge es hier zum Dach.")

# all directions
directions = ("norden", "osten", "süden", "westen", "oben", "unten")

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

Jedes Spiel hat einen Zustand. Zur Zeit müssen wir uns nur den aktuellen Raum merken.

Ich bin unschlüssig, ob ich das Spiel als Exemplar einer Klasse Game modellieren soll oder einfach eine globale Variable und Funktionen benutzen soll. Ich glaube, letzteres ist für den Anfang einfacher.

# the current room
current_room = None

def enter_room(room):
    global current_room
    current_room = room
    describe_room()

def describe_room():
    emit()
    emit(current_room.name); emit()
    emit(current_room.description); emit()

Die Funktion enter_room setzt den übergebenen Raum als aktuellen Raum und gibt dann den Namen und die Beschreibung des Raums aus. Dazu benutze ich eine Funktion namens emit, die im Prinzip wie print funktioniert, allerdings an Wortgrenzen umbricht, damit die Ausgabe besser aussieht:

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

Ich gehe davon aus, dass die Konsole 80 Zeichen breit ist. Theoretisch könnte man das versuchen vom Betriebssystem zu erfragen, ein Detail, welches ich dem Leser überlasse.

Um das Spiel durchzuführen, setzen wir den Startschauplatz und verarbeiten dann in einer Schleife die Befehle, die wir mittels input (ich benutze Python 3.2, bei Python 2.7 wäre es raw_input) einlesen. Wird "ende" eingeben, beende ich das Spiel.

def play():
    enter_room(vor_der_burg)
    while execute_command():
        pass

def execute_command():
    words = read_command()
    if words:
        if words[0] in ("gehe", "geh"):
            if len(words) > 2 and words[1] == "nach":
                execute_go(words[2])
            elif len(words) > 1:
                execute_go(words[1])
            else:
                emit("Wohin soll ich gehen?")
        elif words[0] == "ende":
            return False
        else:
            emit("Ich verstehe '%s' nicht." % "".join(words))
    return True

def read_command():
    return [word.lower() for word in input("? ").rstrip(".?!").split()]

Ich rufe execute_go auf, wenn "gehe" oder "geh" (was grammatikalisch wohl korrekter ist) mit einer Richtung und einem optionalen "nach" eingegeben wurde. Kommt danach noch mehr, wird dies ignoriert. Wir möchte, kann hier noch besser werden. Einige Spiele hatten damals ausgefeilte Parser, die z.B. "Gehe nach Norden, nimm alles außer der Laterne und gehe zurück." oder ähnliche Sätze verstanden haben.

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:

def execute_go(direction):
    room = current_room.exits.get(direction)
    if room:
        enter_room(room)
    else:
        emit("Du kannst nicht nach '%s' gehen." % direction)

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

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. 

? geh nach norden

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. Doch im Westen führt ein Tor in den Burgfried. Alle anderen 
Zugänge scheinen verschüttet zu sein. 

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():
    words = read_command()
    if words:
        if words[0] in ("gehe", "geh"):
            ...
        elif words[0] in directions:
            execute_go(words[0])
        ...
    return True

Im englischen könnte man sogar einbuchstabige Befehle benutzen, doch auf deutsch ist "o" für "oben" oder "osten" leider nicht 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.

Noch eine Kleinigkeit: Da ich schon alt und vergesslich bin, möchte ich mir die Beschreibung eines Raums auch per Befehl anzeigen lassen können:

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

Das soll für heute erst einmal reichen.

Stefan

#!/opt/local/bin/python3.2
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. Doch 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 "
"einige Treppen kommt ein kleiner Raum, in dem es entsetzlich stinkt. "
"Wäre die restliche Treppe nicht eingestürzt, ginge es hier zum Dach.")
# all directions
directions = ("norden", "osten", "süden", "westen", "oben", "unten")
# connect rooms
vor_der_burg.exits["norden"] = burghof
burghof.exits["süden"] = vor_der_burg
burghof.exits["westen"] = burgfried
burgfried.exits["osten"] = burghof
# the current room
current_room = None
def enter_room(room):
global current_room
current_room = room
describe_room()
def describe_room():
emit()
emit(current_room.name); emit()
emit(current_room.description); emit()
def emit(s="", width=80):
column = 0
for word in str(s).split():
column += len(word) + 1
if column > width:
column = len(word) + 1
print()
print(word, end=" ")
print()
def play():
enter_room(vor_der_burg)
while execute_command():
pass
def execute_command():
words = read_command()
if words:
if words[0] in ("gehe", "geh"):
if len(words) > 2 and words[1] == "nach":
execute_go(words[2])
elif len(words) > 1:
execute_go(words[1])
else:
emit("Wohin soll ich gehen?")
elif words[0] in directions:
execute_go(words[0])
elif words[0] in ("schaue", "schau", "beschreibung"):
describe_room()
elif words[0] == "ende":
return False
else:
emit("Ich verstehe '%s' nicht." % "".join(words))
return True
def read_command():
return [word.lower() for word in input("? ").rstrip(".?!").split()]
def execute_go(direction):
room = current_room.exits.get(direction)
if room:
enter_room(room)
else:
emit("Du kannst nicht nach '%s' gehen." % direction)
play()
Copy link

ghost commented Mar 30, 2019

Danke für das tolle Tutorial. Hat mir den Einstieg in Python und objektorientierte Programmierung erleichtert. Habe dein Beispiel bereits um ein paar weitere Klassen erweitert.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment