Sunday, November 23, 2025

Zork I seziert: Eine Autopsie des Infocom-Quellcodes

Bemerkung: Ich habe diesen Text mit Hilfe von Gemini 3 Pro erstellt. Die Hauptarbeit bestand darin, den Gesamten Quellcode in den Context zu Laden und Fragen dazu zu stellen. Ich finde die Einsichten ganz interessant. Am Anfang wusste ich nicht mal, dass Zork in einem Lisp Dialekt entwickelt wurde. Ausserdem ist faszinierend, dass man den gesamten Spielablauf und die Raetsel mit Diagrammen darstellen lassen kann.

Einleitung: Jenseits der Nostalgie

Microsoft hat kürzlich das Repository von Zork I auf GitHub unter der MIT-Lizenz veröffentlicht. Für Historiker ist das nett. Für Entwickler ist es eine Offenbarung. Was wir hier vor uns haben, ist nicht einfach "Retro-Code", sondern eine archäologische Fundgrube, die zeigt, wie man komplexe Welten in einer Umgebung simuliert, die weniger Speicher hat als das Icon einer modernen Smartphone-App.

Der Quellcode bestätigt, was wir lange vermutet haben: Zork ist keine Ansammlung von IF-Statements. Es ist eine komplexe Simulation, die auf einer virtuellen Maschine läuft. Infocom löste das Problem der Portierbarkeit (Apple II, C64, PDP-10), indem sie nicht das Spiel portierten, sondern die CPU. Sie schufen die Z-Machine. Das Spiel selbst ist Bytecode. Das ist exakt das Prinzip der Java Virtual Machine (JVM), nur dass Infocom es 1979 tat, nicht 1995.

Der Code ist brilliant, aber lassen Sie uns ehrlich sein: Nach modernen Standards ist er grauenhaft. Er ist ein Monolith aus globalem Zustand, Seiteneffekten und Speicher-Hacks. Aber genau das macht ihn so interessant. Er zeigt Engineering unter extremen Zwängen.


Die Sprache: ZIL (Zork Implementation Language)

Um diesen Code zu lesen, müssen Sie verstehen, dass Sie hier kein Standard-LISP vor sich haben. Der Code ist in ZIL geschrieben, einer Sprache, die selbst LISP-Veteranen kurz stolpern lässt.

Der schismatische Ursprung: MDL vs. LISP

ZIL ist ein direkter Abkömmling von MDL (Muddle). MDL spaltete sich in den frühen 1970ern am MIT von der dominanten Maclisp-Linie ab. Während beide den ikonischen Klammer-Wald von Lisp 1.5 erbten, ging MDL einen anderen Weg: Es fokussierte sich auf starke Typisierung und komplexe Datenstrukturen, die Maclisp fehlten.

Infocom nutzte MDL nicht, weil es hübsch war, sondern weil es mächtig war. ZIL ist im Grunde ein kastriertes MDL. Man nahm die Sprache, schrieb das Spiel und nutzte dann einen Compiler (ZILCH), um die gesamte schwere LISP-Laufzeitumgebung (Garbage Collection, EVAL) wegzuschneiden. Übrig blieb nackter, kompakter Bytecode für die Z-Machine.


Diagram mit der Einordnung von MDL und ZIL in die Entwicklungsgeschichte von Lisp Dialekten

Interessant ist, was MDL hinterlassen hat. MDL mag den evolutionären Krieg gegen Maclisp (der Vorfahre von Common Lisp) und Scheme verloren haben, aber es starb nicht spurlos. Das berüchtigte, extrem gesprächige LOOP-Makro in modernem Common Lisp? Das stammt direkt aus MDL. Ebenso das Konzept eines robusten Condition-Handling-Systems. Wenn wir ZIL lesen, lesen wir die letzte kommerzielle Festung eines "toten" LISP-Zweigs, der die Industrie mehr geprägt hat, als viele wissen.

Syntax und "Alien Technology"

Wer LISP kennt, erwartet (funktion args). ZIL macht das anders. Es nutzt eine visuelle Unterscheidung, die den Code für das ungeübte Auge wie einen XML-Unfall aussehen lässt, aber einen strikten Zweck erfüllt:

  • < > (Spitze Klammern): Bezeichnen einen Funktionsaufruf oder eine Formauswertung.
    Beispiel: <SETG LIT T> (Setze globale Variable LIT auf True).
  • ( ) (Runde Klammern): Bezeichnen Listen und Datenstrukturen.
    Beispiel: (FLAGS TAKEBIT BURNBIT) ist keine Funktion, sondern eine Liste von Flags.

Diese Unterscheidung macht den Code beim Parsen im Kopf etwas schneller, sobald man sich daran gewöhnt hat.

Makros als DSL

Da Speicherplatz auf Disketten (ca. 160 KB) die härteste Währung war, nutzt ZIL exzessive Meta-Programmierung. Dateien wie gmacros.zil zeigen, dass ZIL oft eher als Domain Specific Language (DSL) fungiert.

Ein Befehl wie <TELL "Hello"> ist kein Funktionsaufruf zur Laufzeit. Es ist ein Makro, das zur Kompilierzeit in eine optimierte Serie von Z-Machine-Opcodes (wie PRINTI) expandiert wird. Der Entwickler schreibt High-Level-Code, der Compiler erzeugt Low-Level-Assembler.



Darstellung der Code Pipeline in Zork.

Diese Pipeline ist der Grund, warum der Code so aussieht, wie er aussieht. Es ist keine Sprache für Puristen. Es ist eine Sprache für Ingenieure, die ein Universum in eine Streichholzschachtel pressen müssen.


Die Architektur: Ein geniales Chaos

Wer Spaghetti-Code erwartet, wird überrascht sein. Die Architektur von Zork ist modular, durchdacht und – für 1980 – fast schon modern. Infocom hat nicht einfach ein Spiel geschrieben; sie haben eine Engine gebaut.

Der Code teilt sich strikt in zwei Welten:

  1. Die generische Engine (g*.zil): Dateien wie gverbs.zil oder gparser.zil definieren die Physik der Welt. Was bedeutet "Nehmen"? Was bedeutet "Öffnen"? Diese Dateien wurden für Zork II, Zork III und andere Spiele wiederverwendet.
  2. Die spezifische Datenbank (1*.zil): Dateien wie 1dungeon.zil und 1actions.zil enthalten den eigentlichen Inhalt von Zork I.

Diese Trennung war der Schlüssel zu Infocoms Produktivität. Sie bauten eine Fabrik für Textadventures.

Das Objekt-Modell: Alles ist ein Ding

In ZIL gibt es keine Trennung zwischen "Räumen", "Gegenständen" oder "Akteuren". Alles ist ein <OBJECT>. Das Datenmodell ist eine strikte Hierarchie (Containment Tree). Der Spieler ist ein Objekt in einem Raum-Objekt, und das Schwert ist ein Objekt im Spieler-Objekt.



Darstellung der in Zork verwendeten Objekte.

Ein Blick in 1dungeon.zil zeigt, wie effizient diese Definitionen sind. Nehmen wir den Startpunkt des Spiels:

<ROOM WEST-OF-HOUSE
      (IN ROOMS)
      (DESC "West of House")
      (NORTH TO NORTH-OF-HOUSE)
      (EAST "The door is boarded and you can't remove the boards.")
      (ACTION WEST-HOUSE)
      (FLAGS RLANDBIT ONBIT SACREDBIT)
      (GLOBAL WHITE-HOUSE BOARD FOREST)>

Hier sehen wir die extreme Optimierung:

  • Flags: RLANDBIT (Land, kein Wasser) oder ONBIT (beleuchtet) sind einzelne Bits. Das spart Speicherplatz gegenüber Booleschen Variablen.
  • Globale Referenzen: (GLOBAL ...) verlinkt Objekte, die hier "sichtbar" sein sollen, ohne physisch im Raum zu liegen (wie das Haus oder der Wald).
  • Action Hooks: (ACTION WEST-HOUSE) ist der entscheidende Hook für die Spiellogik.

Der Game Loop: Das "Intercept"-Muster

Die genialste Designentscheidung in Zork ist die Art und Weise, wie Aktionen verarbeitet werden. Es ist ein Kaskaden-System, das "Spezifisch vor Generisch" priorisiert.

Wenn der Spieler "Öffne Tür" tippt, passiert folgendes:

  1. Der Parser identifiziert das Verb (V?OPEN) und das Objekt (WOODEN-DOOR).
  2. Die Engine prüft: Hat die Tür eine eigene ACTION-Funktion (FRONT-DOOR-FCN)?
  3. JA: Führe diese aus. Wenn diese Funktion "Handled" zurückgibt, stoppt die Engine.
  4. NEIN: Führe die Standard-Physik aus (V-OPEN in gverbs.zil), die prüft, ob das Objekt überhaupt geöffnet werden kann.


Zustandsdiagram der zentralen Game Loop.

Das erlaubt es den Designern, die physikalischen Regeln jederzeit zu brechen. Die Standard-Logik sagt: "Man kann Türen öffnen." Die Logik der WOODEN-DOOR sagt aber: "Diese Tür ist zugenagelt."

Ein Beispiel aus 1actions.zil:

<ROUTINE FRONT-DOOR-FCN ()
     <COND (<VERB? OPEN>
            <TELL "The door cannot be opened." CR>)
           (<VERB? BURN>
            <TELL "You cannot burn this door." CR>)>>

Hier fängt das Skript den OPEN und BURN Befehl ab, bevor die Standard-Engine versuchen kann, den Status der Tür zu ändern.

Global State: Die Register der Z-Machine

Während die Struktur sauber wirkt, ist die Datenhaltung "schmutzig". ZIL vermeidet lokale Variablen und Parameterübergabe, um den Stack klein zu halten. Stattdessen nutzt das Spiel globale Variablen, die fast wie CPU-Register fungieren.

Die wichtigsten "Register", die wir im Code gefunden haben:

  • PRSA (Parse Action): Das aktuelle Verb.
  • PRSO / PRSI: Das direkte und indirekte Objekt (z.B. "Schlage TROLL mit SCHWERT").
  • HERE: Ein Pointer auf den aktuellen Raum.
  • WINNER: Der Akteur. Meistens der Spieler, aber wenn Sie dem Roboter Befehle geben, wird der Roboter kurzzeitig zum WINNER.

Jede Funktion kann jeden Zustand ändern. Eine Kampf-Routine (I-FIGHT) greift direkt auf die globale Variable LOAD-ALLOWED (Traglast) zu, um den Spieler zu schwächen, wenn er verwundet wird. Das ist klassischer Spaghetti-Zustand – hochgradig effizient, aber ein Albtraum für das Debugging. Ein Fehler in einer Routine kann theoretisch den Zustand eines völlig unbeteiligten Objekts am anderen Ende der Karte zerstören.


Der Parser: Die Illusion von Intelligenz

Für Spieler in den 1980ern wirkte Zork wie Magie. Man konnte tippen: "Nimm das Schwert und töte den Troll damit", und das Spiel verstand es. Wer den Quellcode liest, stellt fest: Es gibt keine echte KI. Es gibt nur einen extrem ausgeklügelten Mustererkennungs-Algorithmus, der darauf trainiert ist, Unklarheiten aggressiv zu beseitigen.

Die Datei gparser.zil ist das schwerste Stück Code im ganzen Repository. Sie verwandelt chaotische menschliche Sprache in saubere Maschinenbefehle.

Die Anatomie eines Befehls

Der Parser arbeitet nicht Wort für Wort, sondern sucht nach grammatikalischen Schablonen. In gsyntax.zil sind diese Schablonen definiert. Ein Blick darauf zerstört die Illusion der Sprachbegabung sofort:

<SYNTAX ATTACK OBJECT (FIND ACTORBIT) (ON-GROUND IN-ROOM)
        WITH OBJECT (FIND WEAPONBIT) (HELD CARRIED HAVE) = V-ATTACK>

Diese Zeile verrät alles über die "Intelligenz" des Spiels:

  1. Synonyme: Egal ob der Spieler KILL, MURDER, SLAY oder HIT tippt – für die Engine ist es alles derselbe interne ID-Code (V?ATTACK).
  2. Constraints (Einschränkungen): Der Parser prüft vor der Ausführung, ob die Objekte logisch sind.
    • Das Ziel muss ein ACTORBIT haben (man kann keinen Stein "ermorden").
    • Die Waffe muss ein WEAPONBIT haben (man kann niemanden mit einer Wurst "töten").
    • Die Waffe muss HELD sein (im Inventar).

Wenn der Spieler "Töte Troll mit Wurst" tippt, scheitert nicht die Kampf-Logik, sondern der Parser bricht ab, weil die Wurst das WEAPONBIT nicht besitzt. Die Fehlermeldung ist generisch, wirkt aber kontextsensitiv.

GWIM: "Get What I Mean"

Die vielleicht wichtigste Routine in gparser.zil ist GWIM (Get What I Mean). Sie ist der Grund, warum sich Zork modern anfühlt.

Wenn der Spieler nur "Öffnen" tippt, fehlt das Objekt. Ein dummer Parser würde "Was öffnen?" fragen. Zork startet GWIM. Die Routine scannt den aktuellen Raum (HERE) nach Objekten, die zum Verb passen. OPEN verlangt ein Objekt mit dem Flag DOORBIT oder CONTBIT (Container).

  • Findet GWIM genau ein passendes Objekt (z.B. eine Tür), führt es den Befehl automatisch aus: "(the door)".
  • Findet es kein Objekt, gibt es einen Fehler aus.
  • Findet es zwei (z.B. eine Tür und eine Kiste), muss es nachfragen: "Welches meinst du, die Tür oder die Kiste?"

Diese Heuristik spart dem Spieler tausende von Tastenanschlägen und erzeugt die Illusion, dass das Spiel "mitdenkt".



Ablaufdiagramm fuer den heuristischen Kommando Parser.

Die Grenzen des Wortschatzes

Ein Blick in die Vokabular-Listen zeigt kuriose Artefakte. Um Speicher zu sparen, schneidet der Parser Wörter oft nach 6 Buchstaben ab. LANTERN ist für das Spiel identisch mit LANTER.

Noch interessanter sind die "Buzzwords" in gsyntax.zil:

<BUZZ A AN THE IS AND OF THEN ALL ONE BUT EXCEPT>

Diese Wörter werden vom Parser einfach ignoriert (außer ALL und EXCEPT in speziellen Kontexten). Der Satz "Nimm das Schwert" wird intern zu "Nimm Schwert". Das Spiel versteht keine Grammatik; es filtert Rauschen heraus, bis nur noch Schlüsselwörter übrig sind.

Der "Orphan"-Status

Was passiert, wenn der Spieler einen Satz abbricht?

Spieler: "Töte"
Spiel: "Wen möchtest du töten?"
Spieler: "Den Troll"

Das ist für einen Computer extrem schwer, da der Kontext ("Töten") im zweiten Schritt verloren ist. gparser.zil löst das durch den "Orphan"-Status (P-OFLAG). Wenn ein Satz unvollständig ist, speichert der Parser das Verb (V?ATTACK) in einem globalen Puffer und setzt das Orphan-Flag. Die nächste Eingabe wird dann nicht als neuer Befehl, sondern als Ergänzung zum gepufferten Verb interpretiert.

Zusammenfassung der Technik: Der Parser ist ein Meisterwerk der Täuschung. Er versteht keine Sprache, er versteht Zustände. Er mappt Eingaben auf eine Matrix aus erlaubten Bit-Flags (WEAPONBIT, DOORBIT, ACTORBIT). Alles, was nicht in diese Matrix passt, wird mit generischen Fehlermeldungen abgelehnt.


Die Spielmechanik: Simulation statt Skript

Wenn man Zork spielt, fühlt es sich oft so an, als würde man ein Buch lesen, dessen Seiten man selbst umblättern darf. Der Code enthüllt jedoch etwas völlig anderes: Unter der Haube läuft eine komplexe Zustands-Simulation, die Elemente aus Rollenspielen, Stealth-Games und Physik-Puzzles kombiniert.

Es gibt keine "Cutscenes" oder fest geskripteten Abläufe. Alles ist Emergenz – das Ergebnis von Systemen, die in Echtzeit interagieren.

1. Der Dieb: Ein KI-Agent in 1980

Der Dieb ist nicht einfach ein Monster, das in einem Raum wartet. Er ist ein autonomer Agent, gesteuert von einer Finite State Machine (FSM) in der Routine I-THIEF.

Jede Runde würfelt das Spiel. Der Dieb kann sich bewegen, stehlen oder den Spieler angreifen. Der Code in 1actions.zil beweist, wie detailliert sein Verhalten ist:

<ROUTINE ROB-MAZE (RM "AUX" X N)
     <SET X <FIRST? .RM>>
     <COND (<AND <FSET? .X ,TAKEBIT>
                 <PROB 40>>
            <TELL "You hear, off in the distance, someone saying
                   \"My, I wonder what this fine " D .X " is doing here.\"" CR>
            <MOVE .X ,THIEF>)>

Das ist faszinierend: Der Dieb scannt tatsächlich den Inhalt von Räumen (<FIRST? .RM>). Wenn er ein wertvolles Item findet (TAKEBIT), besteht eine 40% Chance, dass er es nimmt. Er "teleportiert" es dann in sein eigenes Inventar (<MOVE .X ,THIEF>). Der Spieler verliert Gegenstände nicht durch ein Skript, sondern weil ein NPC sie physisch aufhebt und wegträgt.

Mehr noch: Der Dieb nutzt die Beute. Wenn er das "Juwelenbesetzte Ei" stiehlt, öffnet er es in seinem Versteck (TREASURE-ROOM), weil er – anders als der Spieler – die Geschicklichkeit dazu hat. Dies ist kein Bug, sondern ein essenzieller Teil des Puzzles: Man muss sich bestehlen lassen, um an den Inhalt des Eies zu kommen.




Zustandsdiagram fuer den Dieb in Zork.

2. Fluiddynamik: Das Staudamm-Rätsel

Das komplexeste physische Puzzle im Spiel ist der Staudamm. Es ist ein Paradebeispiel dafür, wie ZIL globale Zustände nutzt, um die Geografie der Welt zur Laufzeit umzuschreiben.

Das System basiert auf zwei globalen Variablen: GATES-OPEN (Schleusen offen) und LOW-TIDE (Niedrigwasser). Wenn der Spieler den Bolzen dreht, startet eine Simulation (I-MAINT-ROOM), die den Wasserstand Runde für Runde verändert.

<ROUTINE I-RFILL ()
     <FSET ,RESERVOIR ,NONLANDBIT>
     <FCLEAR ,RESERVOIR ,RLANDBIT>
     <COND (<EQUAL? ,HERE ,RESERVOIR>
            <JIGS-UP "You are lifted up by the rising river! ...">)>

Hier sehen wir die Map-Mutation live im Code:

  • FSET ,RESERVOIR ,NONLANDBIT: Der Raum "Reservoir" wird offiziell zu Wasser.
  • FCLEAR ,RESERVOIR ,RLANDBIT: Er verliert die Eigenschaft "Land".

Wer sich in diesem Moment im Reservoir befindet, stirbt nicht durch ein Skript, sondern durch die Physik-Engine: Der Raum ist nun "Nicht-Land", und der Spieler hat kein Boot. Die Konsequenz (JIGS-UP) ist unausweichlich.

3. Das Kampfsystem: Mathematik statt Zufall

Kämpfe in Zork wirken zufällig, folgen aber einer harten mathematischen Logik. Die Routine DO-FIGHT greift auf Tabellen zurück, die wie Matrizen in einem Pen-&-Paper-Rollenspiel funktionieren.

Besonders spannend ist die Waffenspezialisierung. Die Tabelle VILLAINS definiert für jeden Gegner eine "beste Waffe":

<GLOBAL VILLAINS
    <LTABLE <TABLE TROLL SWORD 1 0 TROLL-MELEE>
            <TABLE THIEF KNIFE 1 0 THIEF-MELEE> ... >>

Der Code besagt: Gegen den Troll gibt das Schwert (SWORD) einen Bonus von +1. Gegen den Dieb ist das Messer (KNIFE) effektiver. Wer den Dieb mit dem Schwert angreift, kämpft mit einem mathematischen Nachteil (VILLAIN-STRENGTH Routine), ohne es zu wissen. Das Spiel kommuniziert dies nie explizit, es ist eine versteckte Mechanik.

4. Der "Critical Path": Ein unsichtbares Netz aus Abhängigkeiten

Wenn man all diese Systeme zusammennimmt – den Dieb, die Physik, die Rätsel – entsteht ein Graph aus Abhängigkeiten, der den einzigen Weg zum Sieg diktiert. Der Code erzwingt eine nicht-lineare Lösung.

Ein Blick auf 1actions.zil enthüllt harte Logik-Gatter ("Gates"), die den Fortschritt blockieren:

  1. Das Troll-Gate: Man kann das Dungeon nicht betreten, ohne den Troll zu töten (TROLL-FLAG).
  2. Das Staudamm-Gate: Man muss den Damm schließen, um an die Truhe zu kommen, aber man muss ihn wieder öffnen, um den Fluss mit dem Boot zu befahren.
  3. Das Dieb-Gate: Man kann das Ei nicht selbst öffnen (der Code zerstört es sonst: BAD-EGG). Man ist gezwungen, die KI des Diebes gegen ihn zu verwenden.


Ablaufdiagram der wesentlichen Story-Elemente im Spiel Zork.

Das Faszinierende an diesem Code ist nicht, wie alt er ist, sondern wie systemisch er denkt. Infocom hat nicht einfach eine Geschichte geschrieben; sie haben eine Welt simuliert und den Spieler darin ausgesetzt. Die Geschichte ist nur das, was passiert, wenn der Spieler versucht, diese Simulation zu brechen.


Code-Archäologie: Die schmutzige Wahrheit

Wer diesen Code heute liest und moderne Clean-Code-Standards erwartet, wird schreiend davonlaufen. Zork ist ein Produkt seiner Zeit. Speicherplatz war teurer als Gold, und CPU-Zyklen waren rar. Das führte zu Programmierpraktiken, für die man heute in jedem Code-Review gefeuert würde. Doch genau diese "Sünden" machten das Spiel erst möglich.

1. Bit-Packing bis zum Exzess

In einer modernen Sprache würden wir für den Zustand einer Tür (Offen? Verschlossen? Unsichtbar?) einzelne Boolesche Variablen oder ein State-Enum nutzen. In ZIL wäre das Platzverschwendung.

Stattdessen werden Eigenschaften in Bitmasken gepresst. Ein Blick in 1dungeon.zil zeigt Zeilen wie diese:

<OBJECT GRATE
      (FLAGS DOORBIT NDESCBIT INVISIBLE)
      (ACTION GRATE-FUNCTION)>

FLAGS ist hier keine Liste, sondern ein einziges Maschinenwort (16 Bit).

  • DOORBIT: Das Objekt verhält sich wie eine Tür.
  • INVISIBLE: Der Parser ignoriert es, bis es "entdeckt" wird.
  • NDESCBIT: Es wird in der Raumbeschreibung ("No Description") nicht automatisch aufgelistet.

Die Engine prüft diese Bits mit extrem schnellen bitweisen Operationen (BTST, FSET). Das ist maximal effizient, aber wehe dem Entwickler, der vergisst, ein Bit zu löschen. Ein INVISIBLE-Objekt, das man aufheben kann (TAKEBIT), führt zu Geister-Items im Inventar.

2. Toter Code und Phantomschmerzen

Das Repository enthält Artefakte, die zeigen, dass Zork I nicht sauber geplant, sondern organisch gewachsen (und geschrumpft) ist. Die Datei zork1.errors (ein Log des Compilers) listet gnadenlos auf:

Symbols unused:
UNTIE-FROM
BREATHE
CYCLOPS-MELEE
...

Im Quellcode selbst finden wir die Leichen dieser Funktionen. In 1actions.zil existiert eine Routine BREATHE:

<ROUTINE BREATHE ()
     <PERFORM ,V?INFLATE ,PRSO ,LUNGS>>

Der Code ist da, aber er wird nie aufgerufen. Vermutlich gab es einmal ein Puzzle, bei dem man das Boot mit dem Mund aufblasen musste (daher LUNGS). Im finalen Spiel braucht man zwingend die Pumpe (PUMP). Der Code wurde nie gelöscht, nur "abgeklemmt" – digitaler Müll, der Speicherplatz auf der Entwicklungsmaschine belegte, aber dank des Compilers nicht auf der Diskette landete.

3. Die "Copy-Paste"-Architektur

Infocom entwickelte nicht nur ein Spiel, sondern eine Trilogie. Der Quellcode zeigt, dass sie kein sauberes Versionskontrollsystem (wie Git Branches) hatten, sondern oft denselben Code für alle drei Spiele nutzten und mit IF-Abfragen umschalteten.

Ein Blick in gverbs.zil offenbart diesen Wartungs-Albtraum:

<ROUTINE V-SWIM ()
     <COND (<OR <==? ,ZORK-NUMBER 1>
                <==? ,ZORK-NUMBER 2>>
            <TELL "Swimming isn't usually allowed in the dungeon.">)
           (T
            <TELL "Go jump in a lake!" CR>)>>

Diese Routine prüft zur Kompilierzeit (<COND ...>), welches Spiel gerade gebaut wird. Das bedeutet: Der Code für Zork II und Zork III geistert in den Quelldateien von Zork I herum. Wenn man einen Bug in der Schwimm-Logik von Teil 1 fixte, riskierte man, Teil 3 kaputtzumachen.

4. Der 6-Buchstaben-Hack

Um das Vokabular effizient zu speichern, schnitt der Z-Machine-Standard Wörter oft nach 6 Buchstaben ab. Im Code sehen wir das nicht direkt, aber es erklärt seltsame Kommentare in zork1.errors. Für den Spieler bedeutete das: LANTERN und LANTER waren identisch. Das führte zu dem kuriosen Effekt, dass man oft Tippfehler machen konnte, solange die ersten 6 Buchstaben stimmten, und das Spiel einen trotzdem verstand.


Fazit: Ein Triumph des Engineerings

Der offene Quellcode von Zork I ist entzaubernd und faszinierend zugleich. Er zeigt, dass die legendäre "KI" des Parsers nur ein cleverer Taschenspielertrick aus Bitmasken und Heuristiken ist. Er zeigt, dass die Welt nicht aus Magie besteht, sondern aus globalen Variablen und Zustandsautomaten.

Aber vor allem zeigt er eines: Pragmatismus. Die Entwickler bei Infocom haben keinen "schönen" Code geschrieben. Sie haben Code geschrieben, der funktionierte – unter Bedingungen, die uns heute unmöglich erscheinen (64 KB RAM, langsame Prozessoren). Sie nutzten jeden Trick, jeden Hack und jede Abkürzung.

Zork ist kein Lehrbuchbeispiel für Softwarearchitektur. Es ist ein Lehrbuchbeispiel dafür, wie man ein Produkt ausliefert, egal wie hart die technischen Limits sind. Und das ist eine Lektion, die auch 45 Jahre später noch relevant ist.




No comments:

Post a Comment