(Fortsetzung von hier, wo ich Sunless Sea beschrieben habe. In der ersten Hälfte geht’s ums Prinzip, in der zweiten wird es dann immer technischer.)
Das modulare Prinzip, nach dem in eine bestehende Spielwelt kleine Missionen oder Geschichten eingebaut werden, würde ich gerne für ein Programmierprojekt in der Schule nutzen. Dazu erschafft man gemeinsam eine Fantasy-Welt oder nimmt eine von mir vorgegebene. Die Welt besteht aus vielen Orten, zwischen denen man reisen kann, modelliert als Graph. Jeder Ort hat einen Namen und eine schön geschriebene Beschreibung, jede Verbindung eine Distanz, später können dann gerne noch andere Attribute dazu kommen.
Außerdem sollen Schülerinnen und Schüler so einfach wie nötig Geschichten schreiben können, die in dieser Welt spielen. Wenn man dann eine Klasse von 30 Schülern hat, kriegt man sicher ein Dutzend Geschichten, die man in die Welt setzen kann, und nach und nach können es auch mehr und kompliziertere werden. Aber für den Anfang reichen einfache.
Letztlich ist das auch nichts anderes als das, was man vielleicht mit einem Leveleditor macht, um neue Levels für ein bestehendes Spiel zu erstellen. Aber hier finde ich das einfacher (und als Sprachlehrer reizvoller) dadurch, dass die geschichten vor allem aus Text bestehen und nicht aus der Platzierung von Mauern, Schatzkisten, Waffen, Monstern. Das macht das Programmieren leichter, glaube ich, und erfordert aber mehr kreatives Schreiben.
Schauen wir uns dazu die im letzten Blogeintrag erwähnte Geschichte von Mr Sacks an, dem dunklen Weihnachtsmann. „The Crimson Beast of Winter stirs in your hold…“ In seinem Blog stellt der Autor der Geschichte, Richard Cobbett, sie kurz vor. (Hier schreibt er zur von ihm geschriebenen Affeninsel. Und, auch interessant, hier die Writer Guidelines, nach denen die verschiedenen Autoren Geschichten für die Welt von Sunless Sea schreiben.)
(Es folgen Spoiler, sollte klar sein.)
- Sobald man am Nordpol-Äquivalent gewesen ist, wird bei der nächsten Ankunft in London die Geschichte ausgelöst
- In London macht Mr Sacks einem ein Angebot: Man muss ihn der Reihe nach an drei Orte seiner Wahl bringen. Danach werde man belohnt, es sei aber nicht ungefährlich. Dazu schöne, kurze Prosa über ihn als übernatürliche Schreckensgestalt, der Geschenke einfordert und Übel hinterlässt. Ein einzelner Reiter der Apokalypse sozusagen.
- Das Angebot kann man annehmen oder nicht oder die Entscheidung vertagen.
- Nimmt man es an, erfährt man das erste Ziel. Genauer gesagt: Man hat die Wahl zwischen zwei Zielen.
- Bringt man Mr Sacks zu einem der beiden Orte, kann man ihn seine Geschenke dort abladen lassen oder nicht oder die Entscheidung vertagen.
- Führt er seine böse Tat aus, gibt es wieder etwas Prosa, und man erfährt das nächste Ziel beziehungsweise die nächsten zwei Zielmöglichkeiten.
- Bringt man Mr Sacks zu einem der beiden Orte, kann man ihn seine Geschenke dort abladen lassen oder nicht oder die Entscheidung vertagen.
- Führt er seine böse Tat aus, gibt es wieder etwas Prosa, und man erfährt das letzte Ziel: den Nordpol, wo man ihm bei einem Ritual helfen soll.
- Auf dem Weg dorthin, auf See, erhält man eine Nachricht vom Ministerium und eine Flüssigkeit, mit der man das Ritual sabotieren kann.
- Bringt man Mr Sacks zum Nordpol, hat man die Option, ihn sein Ritual ausführen zu lassen oder es zu sabotieren. In einem der beiden Fälle wird eine neue Geschichte ausgelöst.
- Beim nächsten Besuch in London wartet ein Geschenk von Mr Sacks auf einen. (Eventuell abhängig vom Verhalten zuvor? Das weiß ich nicht, ich habe ja nicht alle Varianten durchgespielt.)
- Außerdem: Während dieser Mission erhält man, mehr oder weniger zufällig oder zeitabhängig, jedenfalls nicht bewusst vom Spieler ausgelöst, drei Besuche vom ungeduldiger werdenden Mr Sacks. Die ersten beiden Mal kann man ihn hinhalten, das dritte Mal nicht.
Diese Geschichte riecht nach der Modellierung mit einem Zustandsdiagramm. Warum? Weil es mir sinnvoll erscheint, weil Zustandsdiagramme und deren Umsetzung im Lehrplan für 10 und 12 stehen, und weil ich gar so viele andere Modellierungsmöglichkeiten überhaupt nicht kenne.
Der nackte Zustandsautomat
Ein erstes Diagramm zur Modellierung der Situation oben könnnte so aussehen:

Die Bedeutung der Zustände:
- Z0: Die Geschichte beginnt, der Auftrag steht im Raum. Man hat die Wahl, ob man ihn annimmt, ablehnt oder erst mal abwartet. Wenn man abwartet, passiert nichts; wenn man ablehnt, landet man im Zustand Z5, wenn man annimmt, in Zustand Z1.
- Z1: Man hat den Auftrag angenommen, aber noch nichts erfüllt. Man hat die Wahl, Mr Sacks und seine Geschenke in Ziel 1 abzuliefern oder nicht. Man hat, weil das so sein soll ist, nicht mehr die Möglichkeit, aus dem Geschäft auszusteigen und in Zustand Z5 zu kommen.
- Z2: Man hat das erste Ziel bereits erreicht, das zweite noch nicht, und hat die Wahl, Mr Sacks in Ziel 2 abzuliefern oder (erst mal) noch nicht.
- Z3: Man hat das zweite Ziel bereits erreicht, den Nordpol noch nicht, und hat die Wahl, Mr Sacks am Nordpol einfach abzuliefern (dann ist man in Zustand Z4) oder sein Vorhaben dort zu sabotieren (dann ist man in Zustand Z5).
- Z4: Man hat Mr Sacks wunschgemäß abgeliefert, aber seine Belohnung – von der man noch nichts weiß – noch nicht in London abgeholt. Sobald man das tut, landet man in Zustand Z5.
- Z5: Die Geschichte ist beendet und wird nicht mehr angezeigt. Es gibt drei Möglichkeiten, wie man in diesen Zustand gelangt.
Die Bedeutung der Zustandsübergänge:
- Von den meisten Zuständen aus kann man in andere Zustände übergehen, abhängig von auslösenden Aktionen. Ich habe die Aktionen hier „annehmen, abwarten, ablehnen“ und so weiter genannt.
Die schwarzen Angaben sind ausgelöste Aktionen, die bei dem Zustandsübergang zusätzlich oder nebenbei ausgeführt werden:
- So soll die Furcht des Spielers um 5 steigen, wenn man den Auftrag annimmt, und wenn man den ersten Schritt ausführt.
- Die Furcht des Spielers um 10 steigen, wenn man den Auftrag ablehnt, und wenn man den zweiten Schritt ausführt.
- Die Furcht soll um 10 sinken, wenn man Mr Sacks sabotiert hat, und um 20 sinken, wenn man ihn nicht sabotiert hat.
- Wenn man seine Belohnung in London abholt, erhält man 20 Geld.
Anmerkungen:
- Die ausgelösten Aktionen können wesentlich komplexer sein, die Werteänderung ist nur ein Beispiel.
- Mein erster Versuch hatte übrigens zwei Zustände mehr, bis ich merkte, dass es ja nur einen einzigen Endzustand braucht, egal wie man dorthin kommt.
- Außerdem kann man in so ein Diagramm auch noch Bedingungen einfügen, die bei einem versuchten Zustandsübergang erfüllt sein müssen. Dass man irgend etwas Bestimmtes mit sich führt, vielleicht. Die habe ich der Einfachheit halber weggelassen.
- Und was ist mit den drei Erscheinungen von Mr Sacks, mit der letzten, tödlichen? Die habe ich vorerst auch weggelassen. Weglassen ist oft eine gute Idee. Aber ich würde sie ohnehin nicht mit einem Zustandsautomaten umsetzen. Man bräuchte dann nämlich Z1 in zwei Varianten, also Zuständen, für erste und zweite Warnung, und Z2 und Z3 ebenso, und vielleicht einen letzten Zustand für die dritte Warnung. Das wäre mir zu umständlich, stattdessen würde ich ein Attribut „Mr Sacks Ungeduld“ einführen und das nach und nach erhöhen (ausgelöst von einem Timer?) und unabhängig von den anderen Zuständen etwas bewirken lassen. Aber erst mal weglassen.
- Auch der Sabotageauftrag des Ministeriums fehlt. Den könnte man sicher als Zustand modellieren. Aber letztlich ändert sich nicht wirklich etwas am Zustand, es gibt auch keine Wahlmöglichkeit für den Spieler, und deshalb lasse ich das sein. Das Erscheinen des Boten ist eher eine zusätzlich ausgelöste Aktion, die beim Übergang von Z2 nach Z3 automatisch ausgelöst wird, wenn auch mit zeitlicher Verzögerung.
In Java würde man diesen Automaten so implementieren (Stoff der 10. und 12. Jahrgangsstufe am Gymnasium in Bayern, fällt Schülerinnen und Schülern nicht schwer):
public class MrSacks {
int state = 0;
/* states:
* 0 an offer
* 1 accepted
* 2 first package delivered
* 3 second package delivered
* 4 successful north pole
* 5 story finished
*/
void receiveMessage(String message) {
if (state==0) {
if (message.equals("annehmen")) {
state = 1;
furchtErhoehen(5);
}
else if (message.equals("ablehnen")) {
state = 5;
furchtErhoehen(10);
}
}
else if (state==1) {
if (message.equals("abliefern")) {
state = 2;
furchtErhoehen(5);
}
}
else if (state==2) {
if (message.equals("abliefern")) {
state = 3;
furchtErhoehen(10);
}
}
else if (state==3) {
if (message.equals("abliefern")) {
state = 4;
furchtErhoehen(-20);
}
else if (message.equals("sabotieren")) {
state = 5;
furchtErhoehen(-10);
}
}
else if (state==4) {
if (message.equals("abholen")) {
state = 5;
geldErhoehen(20);
}
}
}
}
Wenn man die Methode receiveMessage
jeweils mit den richtigen Argumenten aufruft, durchlebt man quasi die Stationen der Geschichte. Allerdings ist das ja nur das nackte Gerüst der Geschichte. Lebendig und verständlich wird sie erst durch Text, und der fehlt noch.
Der Text der Geschichte
Wir haben: eine Methode, die abhängig von einer Nachricht und dem aktuellen Zustand die Zustände ändert und eventuell noch anderes auslöst.
Wir brauchen: eine Methode, die abhängig vom aktuellen Zustand und dem aktuellen Ort dem Spieler einen bestimmten Text anzeigt und (mehrere) Entscheidungsmöglichkeiten anbietet – und wenn sich der Spieler für eine Möglichkeit entscheidet, muss das unserer ersten Methode kommuniziert werden, damit die Entscheidung dann auch ausgeführt werden kann. Das wird etwas komplizierter.
Zentral ist für mich die Klasse Report
. Ein Report ist der Bericht einer Geschichte zu einem bestimmten Ort im Spiel, zu einem bestimmten Zeitpunkt, also abhängig vom Zustand der Geschichte. Ein Report enthält eine kurze Beschreibung einer Situation und beliebig viele Optionen. Aus diesen Optionen werden dann später zum Beispiel die Knöpfe, mit denen der Spieler Entscheidungen treffen kann. Eine Option (ein Knopf) erhält einen Text (eine Beschriftung, die der Spieler sieht) und eine Message (eine Nachricht, die der Spieler nicht sieht, die aber losgeschickt wird, sobald der Spieler sich für diese Option entscheidet).
So könnte der View einen Report darstellen. Oder ganz anders: Hauptsache, er hat alle nötigen Informationen dazu.
Wenn der Spieler an einem Ort ankommt, werden alle mit dem Ort verbundenen Stories gebeten, einen Report
abzugeben. Der kann auch mal null
sein, wenn es zu diesem Zeitpunkt nichts zu sagen gibt. Alle Reports zu diesem Ort werden vom Model gebündelt und an den View geschickt, damit der die Reports darstellen kann. Dazu hat der View eine Methode showReports
. Mehr braucht es erst mal nicht. Der View legt zum Beispiel für jeden Report einen eigenen Absatz an, und am Ende jedes Reports wird für jede Option des Reports ein Knopf mit einer Nachricht dahinter angelegt, den der Spieler drücken kann.
Im vereinfachten Klassendiagramm, noch ohne Darstellung der Bewegungsmöglichkeiten, sieht das so aus:

Und in Java wäre die Methode für die Geschichten-Klasse dann so:
Report createReport(String location) {
Report r = null;
if (location.equals("London")) {
if (state==0) {
String message = "Hohoho! Willst du mich an drei Orte meiner Wahl bringen?";
Option o1 = new Option("Ja","annehmen");
Option o2 = new Option("Nein","ablehnen");
Option o3 = new Option("Später","abwarten");
r = new Report("Weihnachtsmonster", message, o1, o2, o3);
}
else if (state==4) {
String message = "Der Weihnachtsmann hat dir ein Geschenk dagelassen. Willst du es öffnen?";
Option o1 = new Option ("Ja", "abholen");
r = new Report("Weihnachtsmonster", message, o1);
}
}
else if (location.equals("Anaheim")|| location.equals("Argentinien") ) {
if (state==1) {
String message = "Hohoho! Ist es in Ordnung, wenn ich mein dunkles Geschenk hier abgebe?";
Option o1 = new Option("Ja","annehmen");
Option o2 = new Option("Nein","abwarten");
r = new Report("Weihnachtsmonster", message, o1, o2);
}
}
else if (location.equals("Beorns Halle") || (location.equals("Britannien"))) {
if (state==2) {
String message = "Hohoho! Ist es in Ordnung, wenn ich mein dunkles Geschenk hier abgebe?";
Option o1 = new Option("Ja","annehmen");
Option o2 = new Option("Nein","abwarten");
r = new Report("Weihnachtsmonster", message, o1, o2);
}
}
else if (location.equals("Nordpol")) {
if (state==3) {
String message = "Du hast die Gelegenheit, dem Weihnachtsmonster zu helfen oder seine Pläne zu sabotieren.";
Option o1 = new Option("Helfen","abliefern");
Option o2 = new Option("Sabotieren","sabotieren");
r = new Report("Weihnachtsmonster", message, o1, o2);
}
}
return r;
}
Natürlich wird die Geschichte besser, wenn man mehr Reports einbaut. Und was noch fehlt, sind Antworten des Weihnachtsmann, durch die man überhaupt erst erfährt, dass man zuerst nach Anaheim oder Argentinien, danach zu Beorns Halle oder nach Britannien, und danach zum Nordpol muss. Gemach, gemach.
Soweit, wie ich das jetzt geschildert habe, existiert auch ein Prototyp. Funktioniert, einschließlich der automatischen Anzeige von Bewegungsmöglichkeiten: für jeden Nachbarort einen Knopf, der einen nach dem Klick dorthin befördert. Die Reports werden in einer TabbedPane angezeigt, also mit kleinen Reitern oben. (Scrollbarer Text wäre eine Alternative.) Jeder Report hat unten die Buttons mit den Optionen.
Mein Hauptproblem ist noch Folgendes. Ich finde es sehr unelegant, zwei verschiedene, aber doch ähnliche Methoden zu haben. Methode 1 (createReport
), um dem Spieler anzubieten, was er für Aktionen auslösen kann. Methode 2 (receiveMessage
), um die Entscheidung des Spielers dann umzusetzen. Das Bindeglied ist ist message
, die letztlich mittelbar von Methode 1 gesendet und von Methode 2 empfangen wird:

Am liebsten hätte ich nur eine Methode, Methode 1 (createReport
), in der schon unmittelbar drinsteht, was ausgeführt werden soll, wenn der Spieler sich für eine Option entscheidet. Dazu müsste ich entweder Funktionen als Argumente übergeben, was mit Java inzwischen wohl geht, ich aber nicht weiß, ob ich das mit Schülern machen soll. Oder ich müsste ein Objekt einer anonymen inneren Klasse anlegen (zu einer Unterklasse von, sagen wir, Result
, mit einer zu überschreibenden execute-Methode). Dann bräuchte ich allein Methode 1 (createReport
), und die Optionen, die dem Report als Möglichkeit übergeben würden, sähen dann so aus:
Report createReport(String location) {
Report r = null;
if (location.equals("London")) {
if (state==0) {
String message = "Hohoho! Willst du mich an drei Orte meiner Wahl bringen?";
Option o1 = new Option("Ja", new Result() {
@Override public void execute() {
state = 1;
furchtErhoehen(5);
}
} );
Option o2 = new Option("Nein", new Result() {
@Override public void execute() {
state = 5;
furchtErhoehen(10);
}
} );
Option o3 = new Option("Später", new Result());
r = new Report("Weihnachtsmonster", message, o1, o2, o3);
Ist das für Schüler leichter verständlich als das mit den zwei separaten Methoden? Der Report landet dann letztlich beim View, zusammen mit den verschiedenen Optionen, zu deren jeder ein Objekt der anonymen Result-Unterklasse gehört, und wenn der entsprechende Knopf gedrückt wird, wird die execute-Methode dieses Objekts ausgeführt. Der Controller wird dabei gar nicht mehr gefragt.

Mit den anonymen Unterklassen geht das ganze übersichtlicher und für die Zukunft auch flexibler, deswegen gefällt mir das besser.
Aber ich sehe die Gefahr, dass die Schüler dann einfach unverstandenen Code abtippen, und das ist ja auch wieder nicht das Ziel.
Vielleicht kann ich das Ende des Schuljahres mit meiner 10. durchspielen. Man könnte die typische Heldenreise damit modellieren oder die Odyssee. Weltraumfahrten oder neblichtes London, die zwölf Heldentaten des Herakles. Eine kleine Gruppe implementiert die Welt, mit schönen Beschreibungen für jeden Ort, Bildern und Musik dazu. Die anderen schreiben Geschichten. EIne Geschichte gebe ich als Beispiel vor. Und dann vielleicht doch noch ein Handelssystem? Gegenstände, die der Spieler mit sich führen kann, auch als Token dafür, dass er schon bestimmte Dinge erledigt hat?
Nachtrag: Ja, das geht auch ohne anonyme Klassen in einer einzigen Methode! Aber die wird dadurch sehr unübersichtlich. Auch keine Lösung.
Schreibe einen Kommentar