Tutorial: Ein Textadventure in Python (Teil 2)
Verfasst: Samstag 28. April 2012, 11:32
Das letzte Mal haben wir die Schauplätze implementiert. Doch eine Sache fehlt noch. Die Geschichte erfordert, dass beim ersten Betreten des Burghofs Vögel auffliegen sollen.
Dies können wir in enter_room einbauen:
Dazu muss natürlich jetzt jeder Raum noch eine Variable visited haben:
Doch einen Spezialfall hier, einen Spezialfall dort und der Quelltext ist schnell unübersichtlich geworden. Außerdem würde ich gerne Funktionen wie enter_room nicht abenteuerspezifisch gestalten müssen.
Wir könnten uns entscheiden, beim Betreten eines Raumes eine Methode enter aufzurufen. Dies würde uns ermöglichen, in einer Unterklasse eine spezielle Implementierung anzugeben. Nun bräuchte aber jeder Raum seine eigene Klasse, denn Python zwingt uns leider dazu, für jedes Objekt eine Klasse zu definieren. In JavaScript, wo ich keine Klassen habe, wäre das ein guter Weg. Doch bei Python missfällt er mir.
Dennoch möchte ich ihn kurz skizzieren:
In beiden Fällen will ich nach der eigentlichen Funktion auf möglichst elegante Weise noch eine weitere Funktion aufrufen. Und eigentlich möchte ich auch noch mehr: Ich möchte nach einem beliebigen Ereignis -- wo das Betreten eines Raumes nur ein Beispiel ist -- eine beliebige Funktion aufrufen können.
Das könnte so aussehen:
Die "Magie" steckt jetzt natürlich in der Funktion after und einem "Rahmenwerk", welches an der passenden Stelle prüft, ob es Ereignisse gibt, die ausgelöst werden müssen.
Das sieht dann so aus:
Hier ist die Implementierung:
Huh. Das könnte verwirrend sein. Beginnen wir am Anfang. In events will ich alle aufzurufenden Funktionen jeweils unter dem Namen des Ereignis (event_type, z.B. "after entering") als Liste speichern.
Die Funktion trigger sucht dann aus events die passende Liste heraus und wenn sie existiert, geht sie alle Funktionen durch und ruft sie der Reihe nach auf. Ich vereinbare, dass eine Funktion, die etwas anderes als "falsch" zurück gibt, dazu führt, dass das Aufrufen der Funktionen abgebrochen wird und dass trigger in diesem Fall auch etwas anderes als "falsch" (jede Python-Funktion, die keine explizite return-Anweisung hat, liefert implizit None zurück, was als "falsch" gilt) liefert. Dies wird später noch nützlich sein.
Die Funktion after ist ein "Decorator", eine Funktion, die eine Funktion liefert, die mit der dekorierten Funktion als Argument aufgerufen wird. Ein Beispiel macht es hoffentlich klarer:
Daher muss after eine Funktion zurückgeben, die ich inner genannt habe. Dieser Funktion wird eine Funktion (func) übergeben. Das erste, was inner macht, ist eine lokale Funktion namens event zu definieren, die prüft, ob das übergebene Argument dem Argument des Decorators entspricht (also der Burghof ist) und in diesem Fall die dekorierte Funktion (also do) aufruft. Andernfalls passiert nichts. Nun kann ich event unter dem passenden Schlüssel in eine Liste einfügen, damit trigger diese Funktion später aufrufen kann.
Nachdem wir dies verstanden haben, möchte ich den Mechanismus noch etwas erweitern.
Es wäre nett, wenn ich einfach
benutzen könnte, um die Logik mit dem visited aus do herauszuziehen:
Ich möchte noch eine weitere Sache ändern. Mir gefällt nicht, dass die Raben einen eigenen Absatz bekommen. Daher möchte ich `emit` wie folgt ändern: Alles was mit `emit` ausgegeben wird, wird zu einem Absatz zusammengefasst. Erst ein explizites `emit()` erzeugt einen neuen Absatz:
Dann muss ich die `emit`s von `describe_room` umstellen:
Und ich brauche ein `emit()` in `execute_command()`:
Und der Text erscheint, wie gewünscht:
Nun fliegen sie, die Raben. So viel Arbeit für ein bisschen Lokalkolorit...
Stefan
Dies können wir in enter_room einbauen:
Code: Alles auswählen
def enter_room(room):
...
if room is burghof and not room.visited:
room.visited = True
emit("Du schreckst ein Dutzend schwarze Raben auf, die sich laut "
"krächzend auf dem Dach des Burgfrieds in Sicherheit bringen.")
emit()
Code: Alles auswählen
class Room:
def __init__(self, name, description):
self.name = name
self.description = description
self.exits = {}
self.visited = False
Wir könnten uns entscheiden, beim Betreten eines Raumes eine Methode enter aufzurufen. Dies würde uns ermöglichen, in einer Unterklasse eine spezielle Implementierung anzugeben. Nun bräuchte aber jeder Raum seine eigene Klasse, denn Python zwingt uns leider dazu, für jedes Objekt eine Klasse zu definieren. In JavaScript, wo ich keine Klassen habe, wäre das ein guter Weg. Doch bei Python missfällt er mir.
Dennoch möchte ich ihn kurz skizzieren:
Code: Alles auswählen
class Room:
def enter(self):
self.describe()
class VorDerBurg(Room):
def describe(self):
emit("Vor dir ragt eine alte trutzige Burg auf...")
class Burghof(Room):
def describe(self):
emit("Die hohen Mauern lassen nur weg Licht...")
def enter(self):
super()
if not self.visited:
self.visited = True
emit("Du schreckst ein Dutzend schwarze Raben auf, die sich laut "
"krächzend auf dem Dach des Burgfrieds in Sicherheit bringen.")
emit()
vor_der_burg = VorDerBurg()
burghof = Burghof()
Das könnte so aussehen:
Code: Alles auswählen
@after("entering", burghof)
def do(room):
if not room.visited:
room.visited = True
emit("Du schreckst ein Dutzend schwarze Raben auf, die sich laut "
"krächzend auf dem Dach des Burgfrieds in Sicherheit bringen.")
emit()
Das sieht dann so aus:
Code: Alles auswählen
def enter_room(room):
global current_room
current_room = room
describe_room()
trigger("after entering", room)
Code: Alles auswählen
events = {}
def trigger(event_type, argument):
funcs = events.get(event_type)
if funcs:
for func in funcs:
if func(argument):
return True
def after(event_type, object):
def inner(func):
def event(argument):
if argument is object:
return func(argument)
funcs = events.setdefault("after " + event_type, [])
funcs.append(event)
return inner
Die Funktion trigger sucht dann aus events die passende Liste heraus und wenn sie existiert, geht sie alle Funktionen durch und ruft sie der Reihe nach auf. Ich vereinbare, dass eine Funktion, die etwas anderes als "falsch" zurück gibt, dazu führt, dass das Aufrufen der Funktionen abgebrochen wird und dass trigger in diesem Fall auch etwas anderes als "falsch" (jede Python-Funktion, die keine explizite return-Anweisung hat, liefert implizit None zurück, was als "falsch" gilt) liefert. Dies wird später noch nützlich sein.
Die Funktion after ist ein "Decorator", eine Funktion, die eine Funktion liefert, die mit der dekorierten Funktion als Argument aufgerufen wird. Ein Beispiel macht es hoffentlich klarer:
Code: Alles auswählen
@after("entering", burghof)
def do(room):
...
=>
def do(room):
...
decorator = after("entering", burghof)
decorator(do)
Nachdem wir dies verstanden haben, möchte ich den Mechanismus noch etwas erweitern.
Es wäre nett, wenn ich einfach
Code: Alles auswählen
@after("entering", burfhof, once=True)
Code: Alles auswählen
def after(event_type, object, once=False):
def wrapper(func):
doit = True
def event(argument):
nonlocal doit
if doit and argument is object:
if once: doit = False
return func(argument)
funcs = events.setdefault("after " + event_type, [])
funcs.append(event)
return wrapper
Code: Alles auswählen
def emit(s="", width=80):
global column
if s:
for word in str(s).split():
column += len(word) + 1
if column > width:
column = len(word) + 1
print()
print(word, end=" ")
else:
column = 0
print("\n")
Code: Alles auswählen
def describe_room():
emit(); emit(current_room.name)
emit(); emit(current_room.description)
Code: Alles auswählen
def execute_command():
emit()
...
Code: Alles auswählen
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. Du schreckst ein Dutzend schwarze Raben
auf, die sich laut krächzend auf dem Dach des Burgfrieds in Sicherheit bringen.
?
Stefan