Devlog: Smashing Pumpkins mit Twine und SugarCube (Teil 3)

Twine
Interactive Fiction
SugarCube
Spieleprogrammierung
Bilder
Stable Diffusion
Künstliche Intelligenz
Autor:in

Jörg Kantel

Veröffentlichungsdatum

19. November 2023

Wie ich gestern schon schrieb, ist die aktuelle Version 0.3 alpha meines kleinen Twine-Spiels über eine kleine Hexe, der ich den Namen »Emmy« gegeben habe, die einen wahnsinningen, Kürbisse vernichtenden Priester auf einem Friedhof stoppen muß, nicht nur die erste spielbare Fassung, sondern auch die erste, in der ich im großen Stil SugarCube-Makros einsetzte. Daher ist es an der Zeit für ein weiteres Devlog:

Wenn man Makros nutzt, ist es in der Regel sinnvoll, die darin verwendeten Variablen beim Start des Spiels mit Werten vorzubelegen. Dafür gibt es in SugarCube eine spezielle Passage StoryInit die beim Start und Neustart eines jeden Spiels im Hintergrund als erstes aufgerufen wird. In der derzeitigen Fassung des Spiels sieht sie so aus:

:: StoryInit [green] {"position":"50,75","size":"100,100"}
<<set $start to false>>
<<set $gateOpen to false>>
<<set $hasPickaxe to false>>
<<set $hasPotion to false>>
<<set $hasBroomstick to false>>
<<set $hasAttacked to false>>
<<set $hasKey to false>>

Der (optionale) Tag green ist mit der Farbe grün verbunden und soll signalisieren, daß es sich um eine Spezialpassage handelt. Und für diese Passagen ist es besonders wichtig, darauf zu achten, daß Groß- und Kleinschreibung zählt. Nur StoryInit wird als Spezialpassage erkannt, nicht jedoch Storyinit, storyinit, storyInit oder jede andere Form der Mischung von großen und kleinen Buchstaben.

In dieser (Spezial-) Passage erkennt man schon folgendes:

Auch wenn dieses Spiel bisher mit dem Variablentyp boolean auskommt, SugarCube kennt noch zwei weitere Variablentypen, nämlich arithmetic und string. Das sieht im Passage-Markup wie folgt aus:

<<set $myBoolean to true>>        /* Boolean */
<<set $myNumber to 3.14>>         /* Arithmetic */
<<set $myName to "Jörg">>         /* String */

Variablen vom Typ boolean können nur die Werte true oder false enthalten (auch hier die Kleinschreibung beachten, True oder False (wie in Python) geht gar nicht), Variblen vom Typ arithmetic können sowohl aus Fließkommazahlen (<<set $pi to 3.14>>) wie auch aus Integerwerten (<<set $health to 10>>) bestehen und für string gilt, daß alles was zwischen zwei Hochkommata ("…") steht, als Text behandelt wird.

Wie das obige Codeschnipsel zeigt, beherrscht SugarCube auch Kommentare, und zwar in folgenden Schreibweisen:

  1. C-Style: /* Dies ist ein Kommentar. */
  2. TiddlyWiki Style: /% Dies ist ebenfalls ein Kommentar. %/
  3. HTML Style: <!-- Und auch dies ist ein Kommentar. -->

Kommentare vom ersten Typ können sowohl im Passage Markup, wie auch in JavaScript- und CSS-Quellcode verwendet werden, während die beiden anderen nur im Passage-Markup gültige Kommentare sind. Daher empfehle ich, aus Gründen der Vereinheitlichung nur Kommentare vom Typ 1 (C-Style) zu verwenden. Sie sind einer Programmiererin oder einem Programierer auch am Geläufigsten.

Variablen machen natürlich nur Sinn, wenn sie auch verändert werden können. Im einfachsten Fall erledigt auch dieses das <<set …>>-Makro, wie zum Beispiel in der Passage Vorraum, in der das Friedhofstor geöffnet wird:

:: Vorraum {"position":"950,500","size":"100,100"}
[img[images/button1.jpg]]
<<set $gateOpen to true>>\

An der Wand des Vorraums erkennt Emmy einen riesigen Schalter. Als sie diesen umlegt,
leuchtet an der Wand ein rotes Licht auf und ein Display unter dem Schalter verkündet:

!!!Gate Open!

Die kleine Hexe verläßt das Pförtnerhaus und schlägt hinter sich die Tür mit einem
lauten Knall zu. Erst dann bemerkt sie, daß sie den Schlüssel zum Haus auf einem
Tisch im Raum vergessen hat.<<set $hasKey to false>>

Egal, Emmy macht sich auf dem Weg zum alten [[Friedhofstor->Tor]].

In dieser Passage wird einmal mit <<set $gateOpen to true>> der Wert von $gateOpen verändert und zum zweiten der mit <<set $hasKey to false>> der Wert von $hasKey, der auf true gesetzt wurde, als die kleine Hexe den Priester besiegt und den Schlüssel an sich genommen hatte, wieder zurück auf false gesetzt, weil unsere Heldin den Schlüssel auf einem Tisch im Vorraum von liegen gelassen hatte. (Damit will ich verhindern, daß Emmy den Vorraum noch einmal betreten kann, wenn sie den Toröffnungsmechanismus schon einmal betätigt hatte.)

Twine hat außerdem die Eigenart, Zeilen, die nur Makros und keinen sichtbaren Text enthalten, als Leerzeilen anzuzeigen, ein Problem, das erst mit dem Storyformat Chapbook zufriedenstellend gelöst wurde. Bei den anderen Storyformaten muß gegen überflüssige Leerzeilen dagegen mit Tricks gekämpft werden: Harlowe zum Beispiel verwendet geschweifte Klammerpaare ({…}), um die Leerzeichen in Makros zu unterdrücken, während man in SugarCube dies am einfachsten durch einen Backslash (\) am Ende der jeweiligen Zeile erreicht.

Dabei sind immer wieder Tests nötig, denn Twine mit SugarCube oder Harlowe ist manchmal sehr eigenwillig, wann es eine Leerzeile setzen will und wann nicht. Zum Beispiel habe ich an der Passage Gräberfeld2 sehr lange probiert, bis die Kombination von Ersetzung durch Makros und Leerzeichen zufriedenstellend angezeigt wurde:

:: Gräberfeld2 {"position":"950,175","size":"100,100"}
[img[images/graeberfeld2.jpg]]

Das Feld am Ende des Pfades wirkt noch älter. Es wird von uralten, verwitterten
Bäumen umsäumt und nur noch wenige Gräber haben die Jahre überdauert.

<<if not $hasBroomstick>><<linkappend "An einem der alten Bäume lehnt Emmys
Hexenbesen, den sie schon die ganze Zeit vermißt hat. ">>Emmy nimmt den Besen an
sich.<<set $hasBroomstick to true>><</linkappend>>

Es gibt von hier aus keinen weiteren Weg als den Pfad, den sie gekommen war,
zurück zum [[Gräberfeld->Urnengräber]] mit der Statue.
<<else>>\
Es gibt von hier aus keinen weiteren Weg als den Pfad, den sie gekommen war,
zurück zum [[Gräberfeld->Urnengräber]] mit der Statue.
<</if>>\

Die Herausforderung war hier die Verschachtelung des <<if … else>>-Makros mit den Makros <<linkappend …>> und <<set …>>. <<if …>> prüft ab, ob eine Bedingung erfüllt ist (in diesem Falle, ob die Hexe (noch) keinen Hexenbesen besitzt (not $hasBroomstick)). Dann soll an dem im Makro eingeschlossenen Satz »An einem der alten Bäume lehnt Emmys Hexenbesen, den sie schon die ganze Zeit vermißt hat.« nicht nur der Satz »Emmy nimmt den Besen an sich.« angehängt werden, sondern Twine soll sich auch merken, daß die Hexe ab sofort einen Besen besitzt: <<set $hasBroomstick to true>>. Warum das die Bedingung einleitende <<if>> keinen Backslash benötigt, das <<else>> und das abschließende <</if>> aber doch, darüber mußte ich lange nachdenken.

Ähnlich sieht die Passage Urnengräber aus, die zweite Passage, an der die kleine Hexe bisher ein Item aufnehmen kann:

:: Urnengräber {"position":"800,175","size":"100,100"}
[img[images/monument1.jpg]]

Dieses Feld scheint der älteste Teil des Friedhofs zu sein. Die Gräber sind völlig
heruntergekommen und im Hintergrund erkennt man eine verfallene Kapelle
<<if not $hasPickaxe>>, an deren Mauer eine
<<linkappend "Spitzhacke lehnt. ">> Emmy nimmt die Hacke an sich und befestigt
sie an ihrem Gürtel. Mit dieser Waffe fühlt sie sich nicht mehr so hilflos.
<<set $hasPickaxe to true>><</linkappend>><</if>>

In der Mitte steht eine relativ gut erhaltene, lebensgroße Statue eines Menschen
mit Fledermausflügeln. Hinter der Statue lädt ein [[versteckter Pfad->Gräberfeld2]]
zu weiteren Erkundungen dieses alten Friedhofteiles ein.

Der Weg in den [[Süden->Tor]] führt zum alten Friedhofseingang mit seinem
riesigen Tor, im [[Westen->Brunnen]] hört sie das Plätschern eines Brunnens.

Da hier das das <<linkappend …>> einschließende <<if …>>-Makro jedoch keinen <<else>>-Zweig benötigte, konnten die Makros in den Passagentext eingeschlossen werden, und so gab es auch keine Probleme mit unerwünschten Leerzeilen. Auch hier wird mit <<set $hasPickaxe to true>> der Wert einer Variablen innerhalb des <<if …>>Makros verändert.

Eine besondere Herausforderung waren dann die Passagen, die den Kampf der Hexe mit dem Priester betrafen. Hier mußte in den umliegenden Passagen Brunnen und Pfad erst einmal abgeprüft werden, ob der Kampf schon stattgefunden hat, um dann die Hexe entweder auf die Passage Gräberfeld oder Gräberfeld leer zu lenken:

:: Gräberfeld {"position":"250,175","size":"100,100"}
[img[images/priest2.jpg]]

Emmy sieht auf einem weiten Gräberfeld den wahnsinnigen Priester mit einer großen
Schaufel auf die Halloween-Kürbisse einschlagen. Er starrt sie mit seinem irren
Gesichsausdruck und glasigen Augen böse an.

<<if $hasPickaxe>>\
Emmy geht davon unbeeindruckt [[auf den Priester zu->Attacke1]].
<<else>>\
Emmy sieht ein, daß sie unbewaffner nichts gegen ihn ausrichten kann. Was soll
sie tun? Soll sie sich an dem Priester vorbei zum [[Brunnen->Attacke1]] im Osten
durchschlagen oder soll sie über den [[Pfad]] im Süden fliehen?
<</if>>

:: Gräberfeld leer {"position":"450,175","size":"100,100"}
[img[images/graeberfeldleer.jpg]]

Emmy steht wieder auf dem Gräberfeld, auf dem sie den Priester besiegt hat.
Nun liegt es friedlich vor ihr. Im Süden sieht sie den schmalen, gewundenen
[[Pfad]] und im Osten plätschert immer noch der [[Brunnen]] an der Friedhofsmauer,
so als ob nichts gewesen wäre.

Wenn der Kampf stattgefunden und die kleine Hexe den Priester besiegt hat (in der derzeitigen Fassung kann die Hexe (noch) nicht verlieren), wird sie auf die Passage Gräberfeld leer geleitet. Dies ist eine stinknormale Passage, in der nichts anderes passiert, als daß sie von hier aus die umliegenden Passagen erreichen kann.

In der eigentlichen Passage Gräberfeld findet jedoch, abhängig davon, ob Emmy mit einer Spitzhacke bewaffnet ist (<<if $hasPickaxe>>) die Vorbereitung zum Kampf statt (Passage Attacke1) oder sie entschließt sich zur Flucht. Falls sie diese in Richtung Brunnen unternimmt, wird sie ebenfalls über die Passage Attacke1 geleitet. In dieser Passage gibt es daher noch eine weitere Verzweigung, je nachdem, ob die Hexe im Besitz der Spitzhacke ist oder nicht:

<<if $hasPickaxe>>\
[img[images/angriff1.jpg]]

Emmy ergreift die Spitzhacke mit ihren Fäusten und schlägt wütend und kräftig
auf den Priester ein. Nach einer kurzen, aber heftigen Gegenwehr
[[bricht er schwer verletzt zusammen->Key]].
<<else>>\
[img[images/priest1.jpg]]

Der Priester bemerkt Emmy und droht ihr mit der Grabschaufel. Emmy weiß, daß sie
unbewaffnet keine Chance gegen den Irren hat und flieht weiter nach
[[Osten zum Brunnen->Brunnen]].

In dieser Passage erkennt man, daß innerhalb von <<if … else>>-Makros nicht nur unterschiedliche Texte stehen, sondern auch jeweils verschiedene Bilder gezeigt werden können. In diesem Fall wird, wenn die <<if>>-Bedinung erfüllt ist, das Bild angriff1.jpg angezeigt, wenn nicht das Bild priest1.jpg. Außerdem wird – je nachdem ob die Bedinung erfüllt ist – die Hexe zu unterschiedlichen Passagen geführt (im Angriffsfall zu Key und im Fluchtfall zu Brunnen).

:: Key {"position":"250,25","size":"100,100"}
[img[images/dead7.jpg]]
<<set $hasAttacked to true>>\
<<set $hasKey to true>>\

Sterbend bricht der Priester zusammen. Emmy nimmt dem kleinen Schlüssel an sich,
den er am Gürtel trug.

Nun hat sie die Wahl. Sie kann zurück zum [[Brunnen]] oder sie kann dem
schmalen [[Pfad]] nach Süden folgen.

Die Passage Key kann nur einmal (nach dem Kampf) betreten werden (wenn nicht, habe ich etwas falsch gemacht). Sie hat auch nur den Zweck, die Variablen $hasAttacked und $hasKey auf true zu setzen.

Ausgestattet mit diesem Schlüssel gibt nun die (weit entfernte) Passage Pförtnerhaus auch den Weg zu dem Vorraum mit dem Schalter zur Toröffnung frei:

:: Pförtnerhaus {"position":"800,500","size":"100,100"}
[img[images/house6.jpg]]

Emmy steht vor dem ziemlich heruntergekommenen Pförtnerhaus. Die Tür zum Haus ist
verschlossen. Links davon steht ein verfallener Schuppen auf Stelzen. In dem
Gerümpel daneben liegt eine alte, rostige Gießkanne.

[[Nördlich->Tor]] erkennt Emmy die Eingangspforte zum Friedhof. Sie kann aber
auch einen [[Weg nach Westen->Vorplatz]] gehen.

<<if $hasKey>>\
Sie nimmt den Schlüssel, den sie dem toten Priester abgenommen hat, und öffnet
die Tür. Sie betritt den [[Vorraum]] des Pförtnerhauses, der in einem rötlichen
Licht schimmert.<</if>>

Hier schließt sich der Kreis: Wenn Emmy den Vorraum (und damit auch das Pförtnerhaus) wieder verläßt, ist das Friedhofstor (Passage Tor) offen und sie kann den Friedhof verlassen (Passage MissionAccomplished):

:: Tor {"position":"800,325","size":"100,100"}
<<if $gateOpen>>\
[img[images/gate_open2.jpg]]

Das Tor ist offen, der Weg ist [[frei->MissionAccomplished]]!
<<else>>\
[img[images/gate_closed2.jpg]]

Das Eingangstor des Friedhofs ist fest verschlossen und es besitzt weder eine
Klinke noch ein Schloß. Irgendwo muß es jedoch einen versteckten Mechanismus
geben, mit dem das Tor aus der Ferne geöffnet werden kann.
<</if>>\

Im [[Norden->Urnengräber]] ist eine Statue im Mondlicht zu erkennen, im
[[Süden->Pförtnerhaus]] liegt die Ruine des alten Pförtnerhauses und nach
[[Westen->Vorplatz]] führt ein Weg zurück zum Vorplatz an der Krypta.

:: MissionAccomplished {"position":"950,325","size":"100,100"}
[img[images/missionacc8.jpg]]

Mission erfüllt. Fröhlich schwingt sich Emmy auf ihren Hexenbesen und fliegt
auf den Brocken zum Hexentanzplatz, begleitet von einigen jubelnden Kürbissen.

!!!Ende

Auch in der Passage Tor gibt es je nach Bedingung, ob <<if $gateOpen>> oder nicht, entweder ein Bild des geschlossenen Tores oder ein Bild eines (halb-) offenen Tores – jeweils mit entsprechendem Begleittext.

Zum Ende des Spiels gibt es noch eine kleine Ungereimtheit. Die Hexe kann durchaus die Passage Gräberfeld2 wieder verlassen, ohne den Besen mitzunehmen. Das wird aber bisher noch nicht wieder abgefragt, die kleine Hexe kann sich also auf einen Besen schwingen, den sie noch gar nicht besitzt. Hier habe ich eine Korrektur in der nächsten Version vorgesehen. Ich wollte erst einmal eine durchspielbare Version schaffen, die ich auf Herz und Nieren durchtesten kann.

Wie hier schon erwähnt, habe ich alle Bilder für das Spiel mit dem Stable Diffusion-Ableger Clipdrop von Stability.ai mit dem Modell SDXL 1.0 und dem Stil Comic Book erzeugt und in diesem Flickr-Album abgelegt. Ich habe hunderte von Bildern erzeugt, von denen ich 75 Bilder aufgehoben habe, und von denen bisher zwanzig (inklusive Cover) Verwendung fanden. Die Prompts zu den jeweiligen »Zeichnungen« findet Ihr in den Bildbschreibungen auf Flickr.

Auch wenn das nach viel Aufwand klingt, es hat sich meiner Meinung nach gelohnt. Die ausgesuchten Bilder passen vom Stil her gut in die Geschichte. Wenn man sich darauf einläßt und ein wenig mit den Prompts spielt, ist die Bildgenerierung mit Hilfe der gekünstelten Intelligenzia eine echte Alternative für jene wie mich, denen eine zeichnerische Begabung nicht in die Wiege gelegt wurde.

Den Quellcode habe ich mit den verwendeten Bildern sowohl als HTML- wie auch als .twee-Datei in meinem GitHub-Repositorium abgelegt. Und damit Ihr das auch testen könnt (das ist ja der Sinn einer frühen Version) gibt es das auch als spielbare Fassung auf Itch.io. (Auch wenn SugarCube nicht wirklich ein responsives Storyformat ist, funzt es in der derzeitigen Fassung sogar auf meinem Android-Handy (auch im Hochformat). Allerdings muß man am Ende des Spiels das Mobilphone aus- und wieder einschalten, sonst hängt man fest. Das werde ich in der endgültigen Fassung noch mit dem Einbau eines externen Links zurück zu Itch.io ändern.)

Was kommt noch? Vorgesehen habe ich ein bad ending, in dem die Hexe den Kampf gegen den Priester auch verlieren kann. Die Überlebenschancen eröhen sich mit der Einnahme des Zaubertranks, dessen Flasche bisher noch sinnlos am Brunnen herumliegt. Und dann will ich noch eine Inventarliste anzeigen lassen (das ist zwar bei dem Mini-Inventar, das die Hexe mit sich herumschleppt, etwas overkill, aber da SugarCube das bietet und das Storyformat das Zeug dazu hat, auch aufwendigere Abenteuer zu entwickeln, möchte ich Euch zeigen, wie man so etwas realisiert). Daher: Still digging!


Bild: Smashing Pumpkins Cover, erstellt mit Stable Diffusion XL Clipdrop. Prompt: »a beautiful young witch with red long hair, sexy dressed, short skirt, blouse with a deep neckline, green eyes, big boobs, and a pickaxe as weapon fights against an engry old priest with white hair, white beard and steel-gray eyes with a grave showel. Halloween pumpkins with carved faces that are lying around the field. The graves and grave monuments are old and weathered. A few old, large trees line the field. A full moon shines above everything, with bats fluttering around it. colored french comic style«, Model: Stable Diffusion XL, Style: Comic Book.