Modulares Geschichtenerzählen in Java, Fortsetzung

(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.)

  1. Sobald man am Nordpol-Äquivalent gewesen ist, wird bei der nächsten Ankunft in London die Geschichte ausgelöst
  2. 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.
  3. Das Angebot kann man annehmen oder nicht oder die Entscheidung vertagen.
  4. Nimmt man es an, erfährt man das erste Ziel. Genauer gesagt: Man hat die Wahl zwischen zwei Zielen.
  5. Bringt man Mr Sacks zu einem der beiden Orte, kann man ihn seine Geschenke dort abladen lassen oder nicht oder die Entscheidung vertagen.
  6. 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.
  7. Bringt man Mr Sacks zu einem der beiden Orte, kann man ihn seine Geschenke dort abladen lassen oder nicht oder die Entscheidung vertagen.
  8. 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.
  9. Auf dem Weg dorthin, auf See, erhält man eine Nachricht vom Ministerium und eine Flüssigkeit, mit der man das Ritual sabotieren kann.
  10. 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.
  11. 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.)
  12. 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:

sunless_sea_zustaende

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 enzigen 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 on 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).

sunless_report
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:

sunless_classes

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:

sunless_mein_problem

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.

sunless_report_alternative

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.

Nachtrag: Hier geht es zum fertigen Projekt (2017).

Tagged: Tags

20 Thoughts to “Modulares Geschichtenerzählen in Java, Fortsetzung

  1. Hmmm….

    Ich würde an dieser Stelle keine inneren Klassen verwenden, die Erfahrung aus meinen gehaltenen Tutorien zum Thema sagt mir, dass Schüler/Studenten diese sehr verwirrend finden.

    Auch von tiefen Verschachtelungen und riesigen Argumentlisten würde ich absehen. Sie machen den Code unleserlich und in Grenzbereichen (hohe Speicherauslastung führt zu Problemen) schwer zu debuggen. Meines Erachtens eine große Schwäche von Java-Code im allgemeinen.

    Die getrennten Methoden finde ich in diesen Fall aus zwei Gründen eine gute Lösung, auch wenn sie unelegant aussieht:
    – Modularer Aufbau. Ein Programmierer kann an der GUI-Implementierung arbeiten, ein anderer an dem was wirklich passiert
    – Leicht lesbar, auch für Anfänger
    Was mir daran sauer aufstößt ist der Stringvergleich. Er ist ineffizient und kann bei einer späteren Lokalisierung (übrigens wäre das eine tolle Verbindung zwischen Englisch und Programmierunterricht) zu Problemen führen.

    An der state-machine selbst ist nichts auszusetzen, aber was spricht denn an dieser Stelle gegen einen case-Verteiler anstatt der vielen else-ifs?

    Übrigens würde ich die Strings auslagern, also z.B. aus einer XML-Datei auslesen. Das macht die spätere Lokalisierung oder das ändern der Strings durch einen nicht-Programmierer (den Autor der Story selbst z.B.) einfacher, das spart Entwicklungszeit für die Coder.
    Nachteil ist wieder die Lesbarkeit, aber da kann man mit geschickt gewählten Variablennamen oder Kommentaren aushelfen.

    Ok, und noch eins, dann bin ich erstmal ruhig: IIIHHHHH!!! Wer benutzt denn diese Einrückung!!!111einself1
    (ne, im Ernst, ich verstehe dass das in Java und auch andernorts normal ist, und auch dass es den einen oder anderen Vorteil hat. Aber für mich müssen öffnende und schließende Klammern einfach auf einer Linie stehen. Kernigham und Ritchie haben da was falsch gemacht aus meiner Sicht. :D )

    Gruß
    Aginor

  2. Puh, da bin ich erleichtert, dass ich keine gröberen Schnitzer gemacht habe. Denn: die verschachtelten ifs statt switch sind Absicht; switch/case macht Schülern oft Schwierigkeiten. Ebenso das mit den Stringvergleichen: Ich habe auch an Enums gedacht, oder daran, gleich die Location zu übergeben statt deren Name-Attribut. Aber dann müssten die Schülerinnen sich das Location-Objekt über die Namen geben lassen. Und Strings sind für die Schüler am einfachsten verständlich, wenn auch nicht effizient.

    An Lokalisierung habe ich noch gar nicht gedacht. Sehr schöne Herausforderung!
    Auslagern in XML. Hmja. Siehe Strings oben. Wenn das Projekt mal größer werden sollte, dann sicher. Ich mache mal einen Probelauf ohne, und wenn ich das dann wiederverwenden will, baue ich um.

    Und vielen Dank für den Kommentar, ich brauche da wirklich Beratung!
    Und das mit den Klammern… darüber hat hoffentlich jeder Informatiklehrer schon mal nachgedacht. Rein logisch gebe ich dir so was von recht. Aber, aber, aber… aber so sieht das einfach besser aus! Am liebsten ist mir natürlich Python ohne Klammern… :-)

  3. Guter Einwand mit dem switch/case. Der eine oder andere stolpert da schon drüber…

    Das ist wie mit diesen ternären IFs in PHP die viele erfahrene Programmierer so praktisch finden, aber alle anderen hassen sie:
    $var = 5;
    $var_is_greater_than_two = ($var > 2 ? true : false); // returns true
    Die würde ich in der Schule meiden wie die Pest. Nicht weil sie falsch sind, sondern weil die meisten Leute eine Weile brauchen um sie zu verinnerlichen. Selbst nach Jahren stören die bei mir noch den Lesefluss beim Code lesen, auch wenn ich sie öfters mal benutze.

    Was die Effizienz angeht: In der Größenordnung ist es natürlich akademisch, der PC langweilt sich kolossal die meiste Zeit über. Die Effektivität des lernens ist der Effizienz des Programms hier vorzuziehen, da stimme ich absolut zu.
    Das ist wie beim sortieren: SelectionSort ist bei n=1000 geradeso gut wie QuickSort (aus Sicht eines Menschen), und VIEL leichter zu begreifen.

    Und ja die verschiedenen Einrücker…
    Wenn man in einer Firma arbeitet, wo die Werkstudenten K&R-Einrückung nutzen, die meisten Programmierer Allman, und die älteren Programmierer Whitesmith, und jeder vehement seines bevorzugt, dann wird das irgendwann ein Riesenfass das man aufmacht. Spaß garantiert! :D
    Statistisch gewinnen die K&R-Leute übrigens. In Java, im Linux-Kernel und vielerorts ist das am verbreitetsten, ist hauptsächlich Gewöhnungssache.
    Ich nehme an sich fast alles, nur Horstmann ärgert mich ungemein. Aber dafür gibts ja automatische pretty-printer. :)

    Gruß
    Aginor

  4. Ich hab mal gelernt (naja, gelesen), dass man sich nicht so anstellen und an den Stil des Team oder der Firma anpassen soll. Ternäre Operatoren mag ich selber nicht, habe ich auch Schülern noch nie gezeigt. Ich schreibe im ersten Jahr auch nie i++, sondern immer i=i+1, damit die Schülerinnen sehen, was da vor sich geht.

  5. Genauso läuft es meistens ab, der senior programmer gibt den Stil vor, oder es gibt sogar eine schriftliche Richtline. Aber generell gilt: Mach wie Du willst, und lass nachher eben durch Visual Studio oder so das ganze einrücken wie es die Regel verlangt.
    Und an sich ist es eh egal, erfahrene Programmierer können alles lesen. Wenn nicht wird halt schnell umformatiert. Heutzutage kein Ding.

  6. Idee zum Anfreunden mit den zwei separaten Methoden: Dann lege ich gleich zwei Klassen/Interfaces an, StoryStructure und StoryText, die erste hat nur die Methode receiveMessage, die zweite nur die Methode createReport, und eine (Abstract)Story hat von jedem jeweils eine Instanz.

  7. Habe leider gerade gar keine Zeit, aber ganz kurz: Super Projekt!
    Was mir auffällt ist, dass sich der Zustandsautomat im Klassendiagramm gar nicht wiederfindet. Deshalb brauchst du auch so viele if’s. Das ist m.E. oft ein „code smell“ für noch zu wenig Objektorientierung, insb. zu wenig Polymorphie. Deine Variante mit den inneren Klassen geht schon in die Richtung, wie ich es machen würde.
    Aber: Warum nicht eine Klasse State? Jeder konkrete Zustand hat eine Methode mit Signatur „State transition(Option a)“. Konkrete Übergänge beschreibst du dann mit anonymen Unterklassen von State. Also z.B. im konkreten Objekt state0 würde „transition(annehmenOption)“ das Objekt state1 zurückliefern (und vorher außerdem die Furcht hochsetzen). Der „game loop“ bestünde nur noch darin, die aktuellen Auswahloptionen (kommen auch aus dem aktuellen State-Objekt) darzustellen und „verarbeiteIrgendwie(currentState.createReport()); Option chosenOption = currentState.getUserAction(); currentState = currentState.transition(chosenOption)“ aufzurufen.

    Entschuldige, falls ich beim schnellen Überfliegen deines Ansatzes und beim schnellen Tippen etwas Entscheidendes übersehen oder dich missverstanden habe. Ich kann das am WE auch mal noch weiter ausführen. Z.B. könnte man die Spielinhalte auch durchaus auch in Dateien (XML oder eigenes Format) auslagern, so dass du nur generischen Code programmierst und das eigentliche Storylet-Design (insb. auch die Zustandsübergänge) nicht als Code, sondern als Daten hast. Textdatei als Level-Editor sozusagen.

  8. Noch ein paar schnelle Kommentare:
    1. Den Automaten insgesamt könnte man auch mit einer Hashtabelle implementieren: (Zustand, Aktion) –> Zustand. Dann hättest du alle Übergänge an einer Stelle definiert. Kann aber auch sein, dass es dadurch schwieriger wird, den Entscheidungsfluss durch das Spiel nachzuvollziehen. Dann eher so, wie in meinem ersten Kommentar.
    2. Die location ist ja eigentlich Teil des Zustands, da das Verhalten auch von ihr abhängt. Du hast also so etwas wie einen Kreuzproduktautomaten: Location x Zustand x Aktion –> Zustand. Möglich wäre also auch, dass ein Zustandsobjekt sozusagen „London im Zustand 0“ repräsentiert. Ob und wie genau man das macht, würde ich davon abhängig machen, ob die „narrativ zusammengehörigen“ Elemente auch im Code beieinander bleiben.

  9. Vielen Dank für den Input, Michael! Zum Automaten: Das ist die schöne Schnittstelle zum Lehrplan, die ich habe und an der ich das Projekt anhängen würde. Im lehrplan steht, aus der Erinnerung, etwas Modellieren und Implementieren von Zustandsautomaten. Zur Implementierung steht nichts konkret drin, und mindestens ein Buch zeigt – als Fußnote/Anhang allerdings – die Implementierung mit dem State Pattern. Aber die Bücher gehen vor allem von einer Implementierung durch zwei verschachtelten bedingte Anweisungen (oder switches) aus, eine für den Zustand und eine für den Input. Deswegen wollte ich erst mal bei den vielen ifs bleiben (die für schwache Schüler leichter zu verstehen sind als switch, glaube ich). Zusätzlich glaube ich, dass die Schüler bei der Syntax von Konrollstrukturen mehr Übungsbedarf haben als bei Objektorientierung. Aber vielleicht mache ich das mit den Schülern auch mal mit States. (Und deswegen will ich auch keine Hashtabellen.)

    Mit State-Unterklassen lassen sich die Stories tatsächlich sehr schön programmieren. Aber dann werden es zu viele. Also bräuchte ich anonyme Unterklassen davon – an die habe ich noch gar nicht gedacht und das werde ich unbedingt ausprobieren. Ganz verstanden habe ich deine Idee noch nicht, aber ich war auch noch nicht am Rechner. (Allerdings bin ich gerade wieder weg von anonymen Klassen, der Schüler zuliebe.)

    Ausgliederung im XML/Json/sonstwas: Wenn das ganze mal groß wird, auf jeden Fall. Zu leicht möchte ich das den Schülern aber nicht machen, die sollen schon irgendwas implementieren müssen.

    Das mit dem Kreuzproduktautomaten ist mir auch schon aufgefallen, die bisherige Lösung gefällt mir nicht. Da suche ich noch. Ich könnte mal anfangen mit einer Story, die nur an einem Ort spielt (ein Händler, ein Orakel, ein Witzeerzähler). Nachtrag dazu: Eine Idee ist auf jeden Fall, createReport ohne Argumente aufzurufen – die Story kann den aktuellen Ort des Spielers vom Rest des Models erfahren, wenn der für sie relevant ist.

    Das BlueJ-Projekt liegt übrigens unter:
    https://sourceforge.net/p/informatik/code/HEAD/tree/
    im Ordner „StoryWorld“ – die anderen Ordner sind diverses andere Zeug, an dem ich herumbastle.

  10. Puh. Michael, nochmal vielen Dank für den Tipp. Ich habe einige Zeit gebraucht, bis ich das umsetzen konnte. Eine Option besteht in dieser Variante aus einem Text für die Anzeige, einem neuen State, und einem Action-Objekt. Jetzt besteht die Geschichte eigentlich nur aus etlichen solchen Zustandsobjekten:

    State z0 = new State() {
      Report createReport(String location) {
        Report r = null;  
        if (location.equals("London")) {
          String question = "Ja oder nein?";
          Option o1 = new Option("Ja!", z1, new Action() {
            void execute() {
              System.out.println("Furcht erhoehen 0");
            }
          });
          Option o2 = new Option("Nein!", z0, new Action() {
            void execute() {
              System.out.println("Furcht erhoehen 1");
            }
          });
          r = new Report(MrSacks.this, question, o1, o2);
        }
        return r;
      }
    };
    

    Gefällt mir. Aber wenn ich das mit Schülern mache, muss ich sie vorher einen regulären Automaten implementieren lassen, mit den vielen ifs. Denn das hier können die Schüler abschreiben/übernehmen, aber nicht selber leisten – anonyme Action-Klassen in anonymen State-Klassen.

    Weitere Änderungen: Jede Location ist jetzt auch eine AbstractStory, weil jeder Ort sinnvollerweise auch eine Geschichte ist oder sein kann. Und – egal mit welchem Zustandsautomaten, falls überhaupt – jede Story liefert jetzt auf Anfrage einen Report mit mehreren Options, der wird dem View übergeben, der schickt das ausgewählte Option-Objekt via Controller zurück an die Story. Es werden also keine Strings mehr herumgereicht, sondern Option-Objekte. (Und der Report kriegt eine Referenz auf die Story, für den View.)

  11. Ich hatte hier seit gestern früh einen halbfertigen weiteren Kommentar rumliegen – aber der ist ganz überflüssig geworden durch deine neue Version. Die finde ich ziemlich prima so! Aber klar, dass sie nicht mehr dafür geeignet ist, Kontrollstrukturen zu üben. Didaktisches Ziel hat immer Vorrang!

    Andererseits kann man anhand dieses Beispiels einige richtig tiefe Diskussionen führen: Die Lösung mit den inneren Klassen erlaubt es, zu Zeitpunkt x Entscheidungen darüber zu treffen, was zu einem *späteren* Zeitpunkt y passieren soll, ohne dass dann nochmal Fallunterscheidungen gemacht werden müssen. Hier, indem direkt der Report beeinflusst wird, der u.U. erst viel später generiert und ausgegeben wird. Möglich wäre aber z.B. auch: x=“Benutzer entscheidet sich für Option 3″, y=“Beim nächsten Treffen mit Mr. Sacks wird sich dieser folgendermaßen verhalten“. Das sind schon echt fortgeschrittene Techniken.

    Ich hab jetzt so viel nebenbei über dein Projekt nachgedacht… jetzt hab ich gerade einfach doch mal angefangen, das auch mal umzusetzen – so ganz ohne äußere Vorgaben. Mal sehen.

  12. >Ich hab jetzt so viel nebenbei über dein Projekt nachgedacht… jetzt hab ich gerade einfach doch mal angefangen, das auch mal umzusetzen – so ganz ohne äußere Vorgaben. Mal sehen.

    Pass bloß auf, das macht süchtig. Nächster Feature-Wunsch, der bei der Action-Variante noch nicht funktioniert: Eine unmittelbare Antwort der Story im View, die nichts mit den Zuständen zu tun hat, also einfach ein „Okay“ oder so. Wenn man nicht gleich weiß, was man will… ich muss dann wohl meinen Zyklus überdenken, sitze schon an Sequenzdiagrammen… (Nachtrag: Auch dafür hat sich eine Lösung gefunden.)

  13. Und wie das süchtig macht… Ich hab mir jetzt mal ein paar Nachtstunden abgeknappst und einen Prototyp gebastelt. Hier steht er: https://gist.github.com/anonymous/3c2dc519c7245a892789 Das sind 5 Dateien, die du einfach in ein gemeinsames Verzeichnis packen kannst (default package), dann sollte MainGame ausführbar sein. Achtung: Java8, weil ich ein paar Methodenreferenzen eingebaut habe, for good measure.

    SchwesternStory.java ist das Gerüst einer kleinen Schauergeschichte, die ich mir währends des Programmierens ausgedacht habe. An deren Beispiel sieht man hoffentlich, wie man die „Engine“ verwenden könnte. Zur Erklärung:

    – Da ich ja keinen Lehrplan zu berücksichtigen hatte, bin ich ganz weg von dem expliziten Automatenmodell gegangen. Stattdessen besteht eine Story (bzw Storylet) aus vielen Events, die „getriggert“ werden, sobald der aktuelle Zustand (letzlich eine Ansammlung von Variablen und ihren Werten) bestimmte Bedingungen erfüllt.

    – Dieses (precondition, effect)-Modell beschreibt sozusagen implizit einen Automaten (nicht notwendigerweise einen endlichen). Intuitiv ist es aber eher sowas wie ein Spielbaum oder Suchgraph.

    – Wenn man explizite Zustände braucht, kann man das auch mit einem Attribut machen. Ich habe z.B. standarmäßig eines namens „id“ vorgesehen, das man dafür verwenden kann, wenn man will.

    – Die Zustandsänderungen können durch Interaktionen herbeigeführt werden, müssen aber nicht. Z.B. kann ein Ereignis zeitbasiert getriggert werden oder auch zufällig.

    – Ich habe versucht, allgemeinen Code maximal von der konkreten Geschichte zu trennen. Die Geschichte (hier: SchwesternStory.java) enthält nur noch Events. Im Prinzip hat man jetzt hier eine DSL für Storylets.

    – Es kommt tatsächlich in SchwesternStory.java kein einziges „if“ mehr vor. Welcher Event getriggert wird, entscheidet letztlich das eine „if“ in findTriggeredEvent(). (Man könnte das z.B. auch so ändern, dass u.U. auch mehrere Events gleichzeitig triggern, so lange sie keine Interaktion erfordern.)

    – Bei den Optionen hab ich statt deiner anonymen Objekte einfach mal Java8-Methodenreferenzen benutzt. Nur um zu zeigen, dass es geht und (für mein Gefühl) syntaktisch hübscher ist als anonyme Klassen mit nur einer einzigen Methode.

    – Die Klasse Event ist generisch: ähnlich wie ArrayList<String> schreibt man z.B. Event<SchwesternState>, damit man auf die speziellen Attribute der SchwesternState-Zustände dieser Story zugreifen kann. Das ginge auch ohne, dann müsste man die State-Variablen aber oft nach SchwesternState downcasten. Das finde ich hässlich und hab’s deshalb mit Generics gemacht. Das macht aber schnell viel Aufwand und wird kompliziert (z.B. List<Function<S, String > > in Zeile 8 von Event.java)

    – Allerdings: die Klasse Story selbst müsste nun eigentlich auch generisch werden (und das hätte Folgen für die Unterklassen, die inneren Klassen usw). Darum hab ich mich gedrückt (geschummelt), indem ich in Story.java einige "raw types" und nicht die generischen Varianten verwendet hab. Wie gesagt, Generics *benutzen* ist toll, aber generische Klassen zu *schreiben* nervt eher. Im Zweifelsfall einfach weglassen. Aber so wie's jetzt ist, kann man die trigger-Bedingungen und Effekte recht hübsch und ohne hässliche Casts aufschreiben.

    – Bis hierhin habe ich nur wenige Klassen gebraucht. Eigentlich nur Story (für den Ablauf), State (für den Zustand, inkl. location u.ä.), Event (für die Transitionen). Report gibt es auch (noch) nicht, weil für meine bisherigen Zwecke noch ausreichend war, die Strings direkt nach Generierung durch die Events auszugeben. MVC? Tja, weiß selbst nicht genau, wo da was anfängt und was aufhört. Entscheidend war mir die Trennung von Spiellogik und Storyinhalten, und die habe ich.

    Wie findest du's? Welche Features fehlen?

  14. Ich bin jetzt erst dazu gekommen, mir das anzuschauen. Vom Schreiben her: Ja, genau solche Geschichten meine ich! Schön! Ich hoffe, ich kann Schüler genug enthusiasmieren, dass auch so etwas herauskommt. (Und dann natürlich große Geschichten über eine ganze Staffel: Geschichten, die andere Geschichten auslösen; Geschichten, die einen globalen Welt-Zustand verändern, anhand dessen dann bestimmte Elemente erscheinen oder verschwinden.)

    Um mir das technisch anzusehen und die Features anzuschauen, brauche ich noch ein paar Tage.

  15. So. Ich find’s sehr elegant gelöst. Für die Schule weniger – das war aber auch nicht dein Ziel. Im Prinzip braucht so ein Spiel tatsächlich ein Modell wie dein (precondition, effect). Zuerst überprüfen, ob ein Option in Frage kommt, dann die Folgen bei der Auswahl. Da kann ich mir dann auch einen Level-Editor vorstellen, mit dem man sich jeweils eine Auswahl zusammenklicken kann.

    Beim konkreten Spiel überlege ich noch: Soll man die Optionen zeigen, auch wenn sie nicht in Frage kommen, aber deaktivieren? Dann sieht man wenigsten, was einem noch fehlt. Für ein reines Geschichtenspiel ist das weniger sinnvoll (für solche Spiele gibt es schöne Tools wie etwa Twine), für ein computerlicheres Handelsspiel, wo man bestimmten Gegenstände finden muss, vielleicht schon.

    >Die Zustandsänderungen können durch Interaktionen herbeigeführt werden, müssen aber nicht. Z.B. kann ein Ereignis zeitbasiert getriggert werden oder auch zufällig.
    Unbedingt nötig!

    Ja, und jetzt schwanke ich. Mache ich weiter mit meinen Automaten, für den Unterricht, oder eher ein Modell, bei dem ich nur eine Reihe von Options mit Requirements definiere (und, schülerverständlich schwieriger, Folgen), darunter auch Location, unter denen sich das System dann selber die passenden heraussucht. So in der Art:

    option1.setText("Du beschliesst, den Auftrag anzunehmen.");
    option1.requires(location, "London"); //man muss in London sein
    option1.requires("time",1886); //die Zeit muss mindestens 1886 sein
    option1.requires("money",10); //man muss mindestens 10 Geld haben
    option1.requires("fame",10,20); //Ruhm muss >=10 und <20 sein
    option1.requires(state,5); // Zustand muss 5 sein (und state ein Objekt!)
    option1.follows("money",-10); //man verliert danach 10 Geld
    option1.follows("fame",5); //man gewinnt danach 5 Ruhm
    option1.follows(state,6); // Zustand ist danach 6
    option1.follows(message, "Okay");
    

    Das mit den vielen Strings ist für die Schüler. Letztlich werden damit quasi-globale Variablen gesetzt, also irgendwelche Marker in einer Map an einer zentralen Stelle. Oder eben auch eigene Marker-Attribute, die als Objektreferenz übergeben werden. Damit ist der Zustandsautomat aber so weit abstrahiert, dass für die Schüler keiner mehr zu erkennen ist. (Dann ist der Schritt nicht mehr weit, eines der vielen schönen fertigen Werkzeuge zum Erzeugen textbasierter Spiele zu nehmen.)

  16. Ich verstehe, warum du schwankst, glaube aber schon, dass du mit den Automaten weitermachen könntest. Mein Gefühl: Die Zustände wären dann die zentralen „set pieces“, Phasen oder Kapitel der Geschichte. Durch die Optionen und durch Nichtspielerereignisse (um mal Rollenspieler- und Programmiererterminologie zu mischen) werden Übergänge ausgelöst, d.h. Zwischentexte, die zu einem dieser Kapitel hinleiten. Fände ich es jetzt nahe genug am Lehrplan. Vielleicht kannst du ja in einer Art Manöverkritik hinterher diskutieren lassen, wo dieses Zustandsmodell an seine Grenzen stößt.

    Die Variante mit den requires()-Methoden wird meinem Gefühl nach nur funktionen, wenn die Argumente noch flexibler angegeben werden können als in deinen Beispielen. option1.requires(„fame“,10,20) könnte ja auch „echt größer als 10 und echt kleiner als 20“ bedeuten. Was, wenn ich beide Varianten brauche — oder noch ganz andere? Läuft am Ende wieder auf die precondition-Lösung raus. Oder natürlich, wenn du mehrere Bedingungen nach und nach hinzufügen willst, wie in deinem Beispiel, eine Liste von preconditions. In Java8 wäre das eine Liste von Predicate-Lambdas: https://docs.oracle.com/javase/8/docs/api/java/util/function/Predicate.html
    Natürlich totaler Overkill für die Schule und viele Anwendungen. Allerdings total normal in funktionalen Programmiersprachen. Vielleicht erleben wir das ja noch, dass es in Java genauso normal wird…

  17. Nochmal zu den Inhalten: Ich fand es sehr spannend, mir dabei zuzusehen, wie meine „zwei Schwestern“ entstanden. Schlussvarianten festlegen ohne zu wissen, was zwischendrin geschehen wird. Textbausteine zu formulieren und dann „…“ dazwischen zu setzen, aus Zeitnot zuerst, aber später auch, weil ich das so anregend für den Leser fand. Namen wegzulassen und nur Pronomina zu verwenden, damit offen bleiben kann, welche Schwester überlebt. Oder auch, an wen der Brief gerichtet ist — denn Milly ist natürlich nur der Kosename für Emilia….

    Hat Spaß gemacht, selbst diese wenige Zeilen Text. Danke für die Inspiration!

  18. >Die Variante mit den requires()-Methoden wird meinem Gefühl nach nur funktionen, wenn die Argumente noch flexibler angegeben werden können

    Ich arbeite im Moment mit zwei Varianten: Automat mit vielen ifs und letztlich einer Empfangen- und einer Senden-Methode (also wie ganz oben beschrieben), und einer Klasse, die fast nur aus requires()- und triggers()-Aufrufen besteht, mit Varianten: requiresAtLeast, requiresNotMoreThan und so weiter. Die Option erzeugt daraus beliebig viele Requirement-Objekte, ähnlich mit Consequence-Objekten durch die triggers-Methoden. (Dahin sind die anonymen Action-Klassen von ganz oben gewandert.)

    Was am Schluss herauskommt, muss ich mir überlegen. Aber das teste ich dieses Halbjahr mit meiner 10. Klasse.

    — Schreiben: Schreiben macht ja grundsätzlich Spaß, finde ich, aber auch Arbeit. Und ordentlichen Code schreiben ist leichter als Prosa, und führt schnelle rzu Erfolgserlebnissen. Und Storylets für Spiele schreiben, oder für Serien, ist wieder etwas ganz anderes, das wir Schülern vorenthalten – dabei dürften die meiste Fiktion, die Schüler rezipieren, Serien-Fiktion sein.

    Deine Schwestern passen auch ganz hervorragend zum Sunless-Sea-Setting. Ich hab’s nicht erwähnt, aber die erste Geschichte, auf die man als Spieler stößt, ist die Geschichte dreier Schwestern, die alleine auf einer Fallen London vorgelagerten Insel leben. Man kann bei jedem Besuch mit der einen, der anderen oder der dritten speisen, erhältlich dabei Gossip oder Lebensmittel und etwas Hintergrundgeschichte. Nach einigen Besuchen hört das auf, und die Geschichte wird… more gothic. Mit Feuersbrunst und Mord und Wahnsinn. Aber auch aus den Ruinen des Hauses macht später eine andere Geschichte noch etwas Gewinnbringendes.

    (Ich muss nochmal nachlesen, ob ich alles bei deiner Geschichte mitgekriegt habe. Bin nur gerade in der Schule ohne github-Passwort.)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.