Mein Computer versteht mich
Die folgenden Seiten beschäftigen sich mit der Programmierung von Parsern – den Programmen, die den Spieler »verstehen«.
Was ist ein Parser? Schlägt man in einem Englisch-Deutsch-Wörterbuch nach, so findet man unter dem Begriff »to parse«: grammatisch zerlegen, analysieren. Oft wird statt Parser auch der Begriff Phraser: etwas ausdrücken oder formulieren, verwendet. In jedem Parser muß zwangsläufig auch ein Phraser enthalten sein. Der Parser ist in der Lage, eingegebene Sätze (zum Beispiel Adventure-Spiel-Befehle) grammatisch zu zerlegen, sie zu analysieren, um letztendlich eine mehr oder weniger intelligente Antwort zu geben. Es wird also erst »geparst« (Satz zerlegt), dann analysiert (Reaktion auf den Befehl) und schließlich »gephrast« (eine Antwort formuliert). Parser gewinnen im Bereich der Adventures und KI-Programmierung (KI=Künstliche Intelligenz) zunehmend an Bedeutung, denn letztendlich sind sie es schließlich, die eine Kommunikation zwischen Mensch und Maschine mittels der menschlichen Sprache ermöglichen. Wenn man sich einmal vor Augen hält, wie kompliziert die menschliche Ausdrucksweise mittels Sprache sein kann, dann kann man sich auch gut vorstellen, wie schwierig es ist, einen »perfekten« Parser zu programmieren. Trotzdem, die Fortschritte, die in letzter Zeit im Bereich Parser-Programmiertechnik gemacht wurden, sind enorm; man bringt immer bessere Parser in immer weniger Speicherplatz unter. Inzwischen scheinen die Parser-Programmierer jedoch vor ernsten Schwierigkeiten zu stehen, besonders die, die Parser für die deutsche Sprache entwickeln wollen.
Zweideutigkeiten
Betrachten Sie einmal folgenden Satz:
»Moritz fotografiert die Frau mit dem Fotoapparat.«
Für uns ist der Fall eindeutig: Moritz hat einen Fotoapparat und fotografiert damit eine Frau. Der Satz kann jedoch auch völlig anders gedeutet und trotzdem völlig richtig werden: Moritz fotografiert eine Frau, die einen Fotoapparat bei sich hat. Dies ist nur eines von zahlreichen Beispielen. Uns soll dieses Problem jedoch nicht weiter berühren.
Unser Ziel ist es, einen Parser zu programmieren, der möglichst schnell ist, einen möglichst großen Wortschatz verwalten kann und dem es auch nicht an Intelligenz mangelt.
Der bekannteste Parser, den es gibt, ist der von Infocom. Zunächst möchte ich jedoch noch den Scott-Adams-Parser beschrieben. Scott Adams, Autor von bekannten Adventures wie Adventure, Pirate Cove, Voodoo Castle, The Hulk etc., hält auch heute noch an seinem 2-Wort-Parser fest. Ihm geht es wohl mehr um knifflige Aufgaben und weniger um großartigen Textumfang und Wortschatz. Bei dem 2-Wort-Parser handelt es sich um einen Parser, der nur Sätze versteht, die aus einem Verb und einem Objekt oder einer Richtungsangabe bestehen, zum Beispiel NIMM SCHWERT, WIRF SEIL etc.
Einen 2-Wort-Parser zu programmieren, bereitet wohl kaum jemand allzu große Schwierigkeiten. Auch die meisten Adventures, wie man sie vom Abtippen aus Zeitschriften kennt (zum Beispiel Zauberschloß), bedienen sich eines solchen Parsers. Der nächste Parser, der von sich Reden machte, ist der, der in Melbourne Houses »The Hobbit« zur Geltung kam. Er versteht wirklich komplexe englische Sätze, Verknüpfungen sowie Unterhaltungen mit Personen, die im Spiel auftauchen. Beispiel: »Say to Gandalf please carry me«. Da sich das gesamte Hobbit-Programm auf einmal samt Grafik im Speicher befindet und nie nachlädt, läßt sich auf eine äußerst geschickte Programmierung schließen. Nun aber zum Infocom-Parser. Die Infocom-Adventures waren die ersten, die ständig auf Diskette zugriffen. Dadurch konnte natürlich auch der Parser entsprechend besser gestaltet werden. Infocom-Spiele haben einen Wortschatz von bis zu 1000 Wörtern. Auch die Intelligenz des Parsers läßt nur äußerst selten zu wünschen übrig. Bis vor kurzem galt der Infocom-Parser als das Beste, was sich mit einem 64-KByte-RAM-Computer verwirklichen läßt. Vor noch nicht allzu langer Zeit wurde von Synapse/Broderbond ein Text-Adventure namens Mindwheel veröffentlicht; ausgerüstet mit einem BTZ-(Better than Zork) Parser, der angeblich bis zu 1500 Wörter verstehen soll. An dieser Stelle erlaube ich mir auch gleich zu erwähnen, daß wir im Rahmen dieses Kurses einen BABAZ-Parser (besser als besser als Zork) entwickeln werden. Ich bin mir sicher, daß dieser Parser wirklich das Optimale ist, was sich mit 64 KByte RAM programmieren läßt. Allerdings hängt die Qualität eines Parsers keineswegs von der Größe des Wortschatzes ab. Werfen wir an dieser Stelle doch einmal einen Blick auf mein erstes Adventure »Gordon Saga«. Der Parser – so denke ich heute – war extrem schlecht programmiert. Trotzdem verstand er Sätze wie zum Beispiel »Ich will nach Norden gehen und das Schwert nehmen« genausogut wie »Nimm schnell das Schwert und renne dann sofort nach Norden«. Auch werden Sätze wie »Schließe die Tür« und sogar »Mache schnell die Tür zu« verstanden.
Besonders erstaunlich ist dies alles, wenn man weiß, daß der Wortschatz von Gordon Saga aus lächerlichen 85 Worten besteht. Sie sehen also: Auch mit geringem Wortschatzumfang lassen sich erstaunliche Parser programmieren.
Falls Sie noch keinerlei Vorstellung davon haben, wie ein Parser funktioniert, also wie Befehlssätze zerlegt, codiert und analysiert werden: alles darüber finden Sie im ersten Adventure-Kurs im 64'er-Sonderheft 2/85.
Der Grundaufbau eines Parsers
Das Grundschema eines jeden Parsers ist in Bild 1 veranschaulicht.

Die Grundlagen der Parsertechnik sind keineswegs spezifisch für den C 64 oder einen 64-KByte-RAM-Computer ausgelegt. Sie gelten für jedes Computersystem – egal, wie groß und wie schnell.
Wie Sie aus Bild 1 entnehmen können, setzt sich der Grundaufbau eines Programmes, das Sprache verstehen soll, aus 4 oder 5 Teilen zusammen. Diese Teile wollen wir nun im einzelnen besprechen:
Befehlseingabe
Wie der Name schon sagt, geht es hier um die Eingabe eines Befehls. Dieser Befehl muß jedoch nicht unbedingt das Format »TUE DIES UND JENES« haben. Er kann genausogut eine Frage, eine Antwort oder eine Bemerkung sein. Unter einem Befehl versteht man also alles, was einem Computer mitgeteilt wird. Die Befehlseingabe besteht in der Regel beim C 64 aus einer modifizierten INPUT-Routine. Bei anderen Computern, die bei ihrer INPUT-Routine von vorneherein Kommas und Anführungszeichen erlauben, muß erst gar nicht lange modifiziert werden. Eine modifizierte INPUT-Routine für den C 64 stellt Listing 22 (im Artikel Dateiverwaltung) dar. In diesem Bereich des Parser-Programms läßt sich kaum noch etwas verbessern. Eine Befehlseingabe muß jedoch nicht unbedingt über Tastatur erfolgen. Manche Adventures können, zumindest teilweise, mit Joystick oder Maus gesteuert werden. Theoretisch wäre auch eine Spracheingabe denkbar. Beide Systeme sind aber derart speicherintensiv, daß wir uns nicht näher damit beschäftigen werden.
Parser
Von der Befehlseingaberoutine aus gelangt der Befehlssatz (den ich auch in meinem zweiten Programmierkurs stets im String BE$ ablegen will) in den Parser. Der Parser selbst besteht im Prinzip aus zwei Hälften: Befehlszerlegung und Befehlscodierung (siehe Bild 2):

Wer meinen ersten Adventure-Kurs gelesen hat, weiß bereits, was hiermit gemeint ist. In der ersten Hälfte wird der Befehlssatz zunächst in einzelne Worte zerlegt. Nun greift der Parser in der Codierungshälfte auf den Wortschatz zurück und codiert jedes einzelne Wort. Er stellt fest, daß es in seinem Wortschatz nicht enthalten ist. Diese Zerlegung/Codierung läßt sich auf verschiedene Art und Weise durchführen. Hierzu später mehr, denn zunächst müssen wir erst einmal wissen, wie ein Wortschatz aufgebaut ist.
Wortschatz
Unter einem Wortschatz versteht man alle Wörter, die ein Adventure beziehungsweise KI-Programm kennt. Bei Eliza jedoch ist der Wortschatz gleich Null – wenn man einmal von den Schlüsselbegriffen und Wörtern absieht. Ich muß an dieser Stelle jedoch auch zugeben, daß sich der Parser meiner Gordon Saga eines kleinen Tricks bedient – er gaukelt dem Spieler mehr Intelligenz vor, als er eigentlich besitzt. Gibt man bei einem Infocom-Spiel beispielsweise den Befehlssatz »Küsse das goldene Schwert« ein, so erhält man mit Sicherheit eine Antwort wie zum Beispiel »Das kann doch nicht Ihr Ernst sein!«. Der Infocom-Parser weiß also, daß es keinen Sinn ergibt, wenn man ein Schwert küßt (Es sei denn, das Schwert ist in Wirklichkeit gar kein Schwert, sondern eine hübsche, verzauberte Prinzessin – lechz – aber lassen wir das lieber!). Woher ein Parser so etwas weiß, werde ich Ihnen später ausführlich erklären!
Betrachten wir noch einmal den Gordon-Saga-Parser: Wenn im Spiel eine Tür vorkommt, so gibt es verschiedene Befehlssatz-Möglichkeiten, um diese Tür zu schließen:
- SCHLIESSE DIE TÜR.
- MACHE DIE TÜR ZU.
- ICH WILL DIE TÜR ZU MACHEN.
Bis heute gibt es keinen anderen deutschen Parser, bei dem eine solche Eingabevielfalt erlaubt ist. Wie schafft der Gordon-Saga-Parser dies, wenn er doch nur über einen Miniwortschatz von 85 Worten verfügt?
Betrachten wir doch einmal einen kleinen Ausschnitt aus dem Wortschatz.
Dort steht gespeichert, daß TÜR ein Objekt ist, dem der Objektwert 1 zugeordnet ist.
Es ist auch festgelegt, daß SCHLIESSE und ZU Verben sind. Da beide die gleiche Bedeutung haben, wird beiden zum Beispiel der Verbwert 8 zugeordnet.
Wenn jetzt einer der 3 obigen Sätze nach den Verben ZU oder SCHLIESSE durchsucht wird, wird immer der Verbenwert (beziehungsweise Verbzahl VE) 8 gefunden.
Bei der Objektsuche wird jedesmal der für TÜR entsprechende Wert 1 gefunden.
Der Parser hat somit zwei Werte gefunden:
Objektzahl = 1
und
Verbzahl = 8
Diese beiden Werte werden dem Analyse-Teil des Parsers übermittelt. Der weiß, daß eine Tür geschlossen werden soll, wenn VE=8 und OB=1. Er überprüft jetzt nur noch zwei Dinge:
— ob überhaupt eine Tür vorhanden ist
und
— ob die Tür nicht bereits zu ist (man kann keine verschlossene Tür schließen).
Schließlich antwortet der Parser mit »Ok. Die Tür ist jetzt ZU.«.
Gibt man bei Gordon Saga jedoch zum Beispiel einen Befehl ein, der unsinnig ist und »Schließe das Schwert« lautet, so schweigt der Parser beziehungsweise antwortet mit »Ich verstehe nicht, was Sie wollen«. Er erkennt also nicht – so wie der Infocom-Parser – daß es unsinnig ist, ein Schwert zu schließen. Entschuldigen Sie bitte, wenn ich hier etwas vom eigentlichen Thema Wortschatz abgewichen bin und schon etwas vorausgegriffen habe, aber Sie sehen ja selbst, wie eng die einzelnen Bereiche des Parsers miteinander verschlungen sind! Unser momentanes Problem ist es, wie man einen Wortschatz anlegt und wie man ihn im Computer unterbringt und verwaltet.
Es gibt verschiedene Möglichkeiten, einen Wortschatz zu programmieren. Die Zahl dieser Möglichkeiten hält sich jedoch in überschaubaren Grenzen.
Ganz anders ist dies bei der Analyse. Hier gibt es unheimlich viele Programmiermöglichkeiten. Der Analyse-Teil des Parsers ist nicht zuletzt auch der Teil, in dem die KI steckt. Die KI entscheidet auch, ob der Parser weiß, wovon er redet, oder ob er nur so tut, als verstünde er, was los ist. Sie sehen also schon, daß der »Parser« des ELIZA-Effektes keinesfalls mit einem Parser verglichen werden kann, den ein gutes Adventure haben soll. Wenngleich dies jedoch eine Kombination beider Möglichkeiten nicht ausschließen darf. Im Rahmen dieses Kurses werden alle Parserbereiche ausführlich behandelt. Es werden alle Möglichkeiten beschrieben, die sich mit einem C 64 und Floppy realisieren lassen. Was den Analyse-Teil anbelangt, so kann ich Ihnen jedoch nur einige Lösungsideen vorstellen (Kursteil Adventureprogrammierung). Jeder muß für sich selbst entscheiden, welches Analysekonzept für ihn das beste ist.
Ausgabe
Im Ausgabe-Teil des Parsers wird schließlich das Resultat der Analyse ausgegeben. Dies geschieht in der Regel in Form einer PRINT-Ausgabe auf dem Bildschirm.
Parser 4.0 ist ein völlig Disketten-orientiertes System. Das heißt, der gesamte Wortschatz befindet sich in Form einer relativen Datei auf Diskette. Eine Diskettenseite faßt 170 KByte. Bei dem Wortschatz können wir davon ausgehen, daß die Wörter (Verben, Objekte etc.) eine Wortlänge von 20 Zeichen nicht überschreiten. Man kann theoretisch also Wortschätze mit über 6000 Wörtern verwalten. Praktisch ist dies jedoch Unsinn, da auf der Diskettenseite ja auch noch der Text für das Adventure untergebracht werden muß. Realistisch ist es allerdings, mit Parser 4.0 Wortschätze mit etwa 500 Wörtern zu verwalten – denn dann ist noch genug Platz für Texte. Lassen Sie uns also jetzt gemeinsam den Disk-orientierten Wortparser 4.0 entwickeln. Bevor man allerdings einen solchen Parser programmieren kann, braucht man erst einmal einen Wortschatz. Wie ist so ein Wortschatz aufgebaut? Die Liste in Tabelle 1 stellt einen solchen Relativen-Datei-Wortschatz dar. Sie wurde mit Hilfe des Relativen-Dateien-Editors zusammengestellt und anschließend ausgedruckt.
| 1 | abwaerts | 2010 |
| 2 | aufwaerts | 2009 |
| 3 | bedanken | 1074 |
| 4 | bedecken | 1037 |
| 5 | befestigen | 1042 |
| 6 | bekleiden | 1065 |
| 7 | belohnen | 1046 |
| 8 | benutzen | 1063 |
| 9 | berauben | 1058 |
| 10 | beruehren | 1017 |
| 11 | bestechen | 1068 |
| 12 | betatschen | 1017 |
| 13 | betrachten | 1011 |
| 14 | betreten | 1018 |
| 15 | betritt | 1018 |
| 16 | bewaessere | 1050 |
| 17 | bewegen | 1025 |
| 18 | bezahlen | 1046 |
| 19 | bieten | 1043 |
| 20 | binden | 1042 |
| 21 | bitten | 1066 |
| 22 | blasen | 1067 |
| 23 | blockieren | 1075 |
| 24 | brennen | 1045 |
| 25 | bruellen | 1070 |
| 26 | buchstabieren | 1016 |
| 27 | buddle | 1015 |
| 28 | danken | 1074 |
| 29 | dechiffrieren | 1038 |
| 30 | druecken | 1051 |
| 31 | entfachen | 1045 |
| 32 | entkommen | 1019 |
| 33 | entkorken | 1034 |
| 34 | entleeren | 1039 |
| 35 | entraetsle | 1038 |
| 36 | entriegle | 1034 |
| 37 | entziffre | 1038 |
| 38 | entzuenden | 1045 |
| 39 | erbitten | 1066 |
| 40 | ergreifen | 1012 |
| 41 | ermorden | 1057 |
| 42 | erschiessen | 1057 |
| 43 | erstechen | 1057 |
| 44 | erwerben | 1032 |
| 45 | erwirb | 1032 |
| 46 | erwuergen | 1057 |
| 47 | finden | 1041 |
| 48 | fliehen | 1019 |
| 49 | fragen | 1066 |
| 50 | fuellen | 1040 |
| 51 | geben | 1043 |
| 52 | gebrauchen | 1063 |
| 53 | gehen | 1001 |
| 54 | gellen | 1070 |
| 55 | gib | 1043 |
| 56 | giessen | 1050 |
| 57 | graben | 1015 |
| 58 | grabschen | 1017 |
| 59 | greifen | 1012 |
| 60 | haendigen | 1043 |
| 61 | heben | 1044 |
| 62 | hinab | 2010 |
| 63 | hinauf | 2009 |
| 64 | hinunter | 2010 |
| 65 | hoch | 2009 |
| 66 | holen | 1012 |
| 67 | huepfen | 1024 |
| 68 | informieren | 1069 |
| 69 | inventur | 1013 |
| 70 | kaufen | 1032 |
| 71 | kitzle | 1075 |
| 72 | klauen | 1058 |
| 73 | koepfen | 1057 |
| 74 | kontrollieren | 1063 |
| 75 | kuessen | 1071 |
| 76 | laufen | 1001 |
| 77 | leeren | 1039 |
| 78 | legen | 1014 |
| 79 | lesen | 1016 |
| 80 | lies | 1016 |
| 81 | liste | 1013 |
| 82 | n | 2001 |
| 83 | nehmen | 1012 |
| 84 | nimm | 1012 |
| 85 | no | 2006 |
| 86 | norden | 2001 |
| 87 | nordosten | 2006 |
| 88 | nordwesten | 2005 |
| 89 | nw | 2005 |
| 90 | o | 2004 |
| 91 | oeffnen | 1034 |
| 92 | osten | 2005 |
| 93 | paddle | 1026 |
| 94 | pennen | 1028 |
| 95 | pfeifen | 1067 |
| 96 | polieren | 1048 |
| 97 | probieren | 1059 |
| 98 | pruefen | 1033 |
| 99 | putzen | 1048 |
| 100 | quetschen | 1051 |
| 101 | ra | 2009 |
| 102 | rasten | 1027 |
| 103 | rauchen | 1079 |
| 104 | rauf | 2009 |
| 105 | reden | 1073 |
| 106 | reiben | 1017 |
| 107 | reinigen | 1048 |
| 108 | reisen | 1052 |
| 109 | reiten | 1029 |
| 110 | rennen | 1001 |
| 111 | ru | 2010 |
| 112 | rudere | 1026 |
| 113 | rufen | 1070 |
| 114 | ruhen | 1027 |
| 115 | runter | 2010 |
| 116 | s | 2002 |
| 117 | saeubere | 1048 |
| 118 | sagen | 1073 |
| 119 | saufen | 1078 |
| 120 | schauen | 1011 |
| 121 | schenken | 1043 |
| 122 | schicken | 1055 |
| 123 | schieben | 1051 |
| 124 | schlafen | 1028 |
| 125 | schliessen | 1035 |
| 126 | schmecken | 1059 |
| 127 | schreien | 1070 |
| 128 | schuettle | 1049 |
| 129 | schwimmen | 1021 |
| 130 | senden | 1055 |
| 131 | setzen | 1030 |
| 132 | so | 2008 |
| 133 | spielen | 1072 |
| 134 | sprechen | 1073 |
| 135 | sprich | 1073 |
| 136 | springen | 1024 |
| 137 | stecke | 1047 |
| 138 | stehen | 1031 |
| 139 | stehlen | 1058 |
| 140 | stoepsle | 1047 |
| 141 | strecken | 1053 |
| 142 | streichle | 1017 |
| 143 | suchen | 1041 |
| 144 | sueden | 2002 |
| 145 | suedosten | 2008 |
| 146 | suedwesten | 2007 |
| 147 | summen | 1067 |
| 148 | sw | 2007 |
| 149 | taetschle | 1017 |
| 150 | tauchen | 1022 |
| 151 | toeten | 1057 |
| 152 | tragen | 1065 |
| 153 | tragen | 1065 |
| 154 | trinken | 1078 |
| 155 | umhuellen | 1037 |
| 156 | unterrichten | 1069 |
| 157 | untersuchen | 1011 |
| 158 | verankere | 1042 |
| 159 | verbinden | 1042 |
| 160 | verbrennen | 1045 |
| 161 | verdecken | 1037 |
| 162 | verdrehen | 1061 |
| 163 | verhuellen | 1037 |
| 164 | verkaufe | 1054 |
| 165 | verknoten | 1042 |
| 166 | verkorken | 1036 |
| 167 | verlassen | 1020 |
| 168 | verlieren | 1014 |
| 169 | verlocken | 1068 |
| 170 | verloeschen | 1062 |
| 171 | vernichten | 1056 |
| 172 | verriegle | 1036 |
| 173 | verruecken | 1025 |
| 174 | verschieben | 1025 |
| 175 | verschliessen | 1036 |
| 176 | versperren | 1036 |
| 177 | versuchen | 1059 |
| 178 | w | 2003 |
| 179 | wandere | 1001 |
| 180 | warten | 1027 |
| 181 | wecken | 1076 |
| 182 | wedeln | 1064 |
| 183 | werfen | 1014 |
| 184 | westen | 2003 |
| 185 | winden | 1060 |
| 186 | winken | 1064 |
| 187 | wirf | 1014 |
| 188 | wuehlen | 1015 |
| 189 | zerquetschen | 1051 |
| 190 | zerreissen | 1052 |
| 191 | zerren | 1052 |
| 192 | zerschlagen | 1056 |
| 193 | zerschmettere | 1056 |
| 194 | zerstoeren | 1056 |
| 195 | zertruemmere | 1056 |
| 196 | ziehen | 1025 |
| 197 | zuenden | 1045 |
Dieser Wortschatz besteht bis jetzt nur aus Verben und Himmelsrichtungsangaben. Diese Verben können als eine komplette Verbensammlung betrachtet werden, die man in einem Adventure benötigt. Das heißt, mit diesen Verben läßt sich nahezu jeder Befehlssatz ausdrücken (in Verbindung mit entsprechendem Objekt natürlich).
Wie liest man diese Wortschatztabelle? Stellen wir diese Frage erst einmal zurück! Nehmen Sie zunächst bitte einmal eine leere Diskette her und formatieren Sie diese. Dann legen Sie mit Hilfe des Relativen-Datei-Editors auf dieser Disk eine relative Datei mit folgenden Angaben an: 600 Sätze mit je 25 Zeichen. In diese relative Datei schreiben Sie dann die Wortschatz-Tabelle. Das geht einfach: Die linke Zahl in der Tabelle gibt jeweils die Satznummer der relativen Datei an. Danach kommt der Satzinhalt: Verbname und Codierungszahl. Bild 3 zeigt ganz genau, wie der erste Satz in der relativen Datei aussehen muß. Die Wörter (=Verben, Objekte etc.) dürfen also maximal 21 Zeichen lang sein. Die letzten vier Zeichen der Sätze sind für die Codierungszahl reserviert, deren Bedeutung uns im Moment noch nicht interessieren soll.

Beim »Abtippen« ist zu beachten, daß Sie nicht vom deutschen Zeichensatz Gebrauch machen. Das heißt, für »ae« darf nicht »ä« gesetzt werden und so weiter.
Wir haben jetzt also einen Wortschatz (bisher zwar nur Verben, aber das ist zunächst egal), auf den wir zurückgreifen können.
In der Einleitung zum Kapitel Parsertechnik habe ich Ihnen bereits den Grundaufbau eines JEDEN Parsers vorgestellt. Am Anfang jedes Parsers kommt folglich die Befehlseingabe, die beim C 64 in der Regel aus einer modifizierten INPUT-Routine besteht.
Nach Durchlauf der Befehlseingabe liegt der Befehl beziehungsweise Befehlssatz als String BE$ vor. Lassen Sie mich nun wieder einmal von meinem alten Lieblingsbefehlssatz ausgehen: NIMM DAS SCHWERT. Also BE$= »NIMM DAS SCHWERT«. Dieser Befehlssatz soll jetzt anhand Vergleich mit dem Wortschatz codiert werden. Das heißt, dem VERB im Satz wird eine Verbzahl VE zugeordnet, dem Objekt eine Objektzahl OB, unbedeutende Wörter (zum Beispiel der, die, das und so weiter) werden ignoriert. Es gibt nun verschiedene Möglichkeiten, einen String beziehungsweise Befehlssatz zu codieren, nach Wörtern zu durchsuchen. Diese Möglichkeiten, ihre Vor- und Nachteile möchte ich Ihnen nun im folgenden einmal ganz genau vorstellen:
Möglichkeit 1
Bitte tippen Sie Listing 1 ab und ergänzen Sie es um Listing 2.
50000 rem *****************************
50001 rem * *
50002 rem * wort-parser 4.0 *
50003 rem * *
50004 rem * <c> 1986 bei *
50005 rem * *
50006 rem * michael nickles *
50007 rem * *
50008 rem *****************************
50010 rem befehlssatzeingabe ---------------------------------------------------
50011 :
50015 sl=80: rem befehlssatzlaenge
50020 print"{down}":be$="":poke198,0:poke211,0:poke214,22:sys58732:print"{yel}{CBM-@}";
50030 getx$:ifpeek(203)=1then50120
50040 ifx$=""then50030
50050 iflen(be$)=0andasc(x$)=20then50030
50060 i=asc(x$):ifi<32ori>133andi<159thenifi<>20then50030
50070 iflen(be$)=slandi<>20then50030
50080 be$=be$+x$
50090 printchr$(20);x$;"{CBM-@}";
50100 ifi=20thenbe$=left$(be$,len(be$)-2):goto50030
50110 goto50030
50120 print"{left} "
50000 REM ***************************** 50001 REM * * 50002 REM * WORT-PARSER 4.0 * 50003 REM * * 50004 REM * <C> 1986 BEI * 50005 REM * * 50006 REM * MICHAEL NICKLES * 50007 REM * * 50008 REM ***************************** 50010 REM BEFEHLSSATZEINGABE --------------------------------------------------- 50011 : 50015 SL=80: REM BEFEHLSSATZLAENGE 50020 PRINT"{DOWN}":BE$="":POKE 198,0:POKE 211,0:POKE 214,22:SYS 58732:PRINT"{YELLOW}@"; 50030 GET X$:IF PEEK (203)=1 THEN50120 50040 IF X$="" THEN50030 50050 IF LEN (BE$)=0AND ASC (X$)=20 THEN50030 50060 I=ASC (X$):IF I<32OR I>133AND I<159 THENIF I<>20 THEN50030 50070 IF LEN (BE$)=SLAND I<>20 THEN50030 50080 BE$=BE$+X$ 50090 PRINTCHR$ (20);X$;"@"; 50100 IF I=20 THENBE$=LEFT$ (BE$,LEN (BE$)-2):GOTO 50030 50110 GOTO 50030 50120 PRINT"{LEFT,SPACE}"
51000 rem befehlszerlegung-codierung ------------------------------------------- 51001 : 51010 vg$="nimm":vg=1012 51020 rem suchen ob vg$ in be$ vorkommt 51030 fori=1 to len(be$)-len(vg$)+1 51040 : if vg$=mid$(be$,i,len(vg$)) then ve=vg 51050 nexti 51060 if ve=0 then printvg$" kommt nicht in be$ vor" 51070 if ve=1012 then printvg$" kommt in be$ vor"
51000 REM BEFEHLSZERLEGUNG-CODIERUNG ------------------------------------------- 51001 : 51010 VG$="NIMM":VG=1012 51020 REM SUCHEN OB VG$ IN BE$ VORKOMMT 51030 FOR I=1 TO LEN (BE$)-LEN (VG$)+1 51040 : IF VG$=MID$ (BE$,I,LEN (VG$)) THEN VE=VG 51050 NEXT I 51060 IF VE=0 THEN PRINTVG$" KOMMT NICHT IN BE$ VOR" 51070 IF VE=1012 THEN PRINTVG$" KOMMT IN BE$ VOR"
Bei dieser Befehlszerlegungs-Methode wird der Befehlssatz eigentlich gar nicht zerlegt. Es wird einfach überprüft, ob das Verb »NIMM« (stellvertretend für die GANZE Worttabelle) im Befehlssatz BE$ enthalten ist. Dazu wird der gesamte Befehlsstring durchlaufen und mittels MID$-Funktion überprüft, ob das Verb NIMM (=VG$) im String vorkommt. Bild 4 demonstriert diesen Suchvorgang für den Befehlssatz »NIMM DAS SCHWERT«: das heißt, nehmen wir lieber als Befehlssatz »OTTO BITTE NIMM ES«, damit es nicht zu einfach wird. Aus Bild 4 wird auch deutlich, warum es legitim ist, anstelle von LEN(BE$) LEN(BE$)-LEN(VG$)+1 zu setzen. Es ist schließlich logisch, daß ein Suchbegriff von 4 Zeichen Länge spätestens in den letzten 4 Zeichen von BE$ gefunden werden muß.

Achtung: Beim bisherigen Programm wird VG$ im gesamten BE$ gesucht. Es wird so zum Beispiel für BE$=»OTTO BITTE NIMM ES«, aber auch in »OTTOBITTENIMMES« gefunden. Will man nach absoluten Wörtern (zum Beispiel ELIZA-Schlüsselwörter) suchen, so muß sichergestellt sein, daß überprüft wird, ob sich am Ende und Anfang des gesuchten Wortes ein SPACE befindet.
Listing 2 muß für diesen Fall so modifiziert werden:
51015 BE$=BE$+" "+BE$+" "
51040 : IF " "+VG$+" "=MID$(BE$,I,LEN(VG$)+2) THEN VE=VG
Methode 1 ist ganz die Methode, die bei ELIZA angewandt wird – nämlich dann, wenn die Schlüsselwörter gesucht werden. Das heißt, alle 38 Schlüsselwörter der ELIZA werden mittels dieser Methode in BE$ gesucht. Nicht zuletzt aus diesem Grund braucht ELIZA ziemlich viel Zeit, bis sie zu einer Antwort kommt. Gehen wir nun einmal von einem Wortschatz für ein Adventure aus, der ca. 600 Wörter groß ist. Ich sagte oben ja bereits, daß das Verb NIMM stellvertretend für den ganzen Wortschatz gesucht wird. Tatsächlich müßten alle 600 Wörter im Befehlssatz gesucht werden. Dies würde viel zuviel Zeit in Anspruch nehmen und außerdem zu Schwierigkeiten führen, wenn sich ein Befehlssatz aus mehreren Einzelbefehlen, die durch UND oder KOMMA getrennt sind, zusammensetzt. Methode 1 ist im Bereich der Adventure-Parser-Programmierung folglich völlig unbrauchbar. Aber auch was die Anwendung bei ELIZA anbelangt, gibt es inzwischen eine wesentlich bessere Methode zum Suchen eines Strings VG$ in einem String BE$:
Möglichkeit 2
Ergänzen Sie diesmal Listing 1 mit Listing 3. Geben Sie auch hier als Befehlssatz BE$ wieder »OTTO BITTE NIMM ES« ein. Auch bei dieser Routine handelt es sich um eine Routine, die den Befehlsstring BE$ durchläuft und VG$ (wieder »NIMM«, stellvertretend für den gesamten Wortschatz) darin sucht. Allerdings erfolgt das Suchen nicht wie bei Methode 1 in »ein Zeichen«-Schritten. Bild 5 verdeutlicht den Suchvorgang bei der hier verwendeten Routine.

Während beim gleichen Befehlssatz BE$ bei Methode 1 ganze 12 Schritte bis zum Finden von VG$ in BES benötigt wurden, sind es hier nur noch 4. Der in Methode 2 angewandte Algorithmus wurde übrigens erst vor kurzem erfunden und in der 64'er Ausgabe 2/85 im Kurs »Effektives Programmieren« beschrieben.
10 dim m%(26)
51000 rem befehlszerlegung-codierung -------------------------------------------
51001 :
51010 vg$="nimm":vg=1012
51020 rem m$-tabelle anlegen ---------
51040 fori=1to26:m%(i)=len(vg$):nexti
51050 fori=1tolen(vg$):m%(asc(mid$(vg$,i,1))-64)=len(vg$)-i:nexti
51052 fori=1tolen(vg$):printmid$(vg$,i,1),m%(asc(mid$(vg$,i,1))-64):nexti
51060 rem vg$ in be$ suchen ----------
51065 print"{clr} ";be$
51070 po=1
51080 fori=1topo:print" ";:nexti:printvg$
51085 ifvg$=mid$(be$,po,len(vg$))thenprint"gefunden !":end
51090 fori=len(vg$)to1step-1
51095 :ifmid$(be$,po+i,1)=""thenprint"nicht gefunden!":end
51105 :ifmid$(vg$,i,1)=mid$(be$,po+i-1,1)then51120
51109 :ifasc(mid$(be$,po+i-1,1))=32thenpo=po+len(vg$):i=0:goto51120
51110 :po=po+m%(asc(mid$(be$,po+i-1,1))-64)+len(vg$)-i:i=0
51120 nexti
51130 goto51080
10 DIM M%(26) 51000 REM BEFEHLSZERLEGUNG-CODIERUNG ------------------------------------------- 51001 : 51010 VG$="NIMM":VG=1012 51020 REM M$-TABELLE ANLEGEN --------- 51040 FOR I=1 TO26:M%(I)=LEN (VG$):NEXT I 51050 FOR I=1 TOLEN (VG$):M%(ASC (MID$ (VG$,I,1))-64)=LEN (VG$)-I:NEXT I 51052 FOR I=1 TOLEN (VG$):PRINTMID$ (VG$,I,1),M%(ASC (MID$ (VG$,I,1))-64):NEXT I 51060 REM VG$ IN BE$ SUCHEN ---------- 51065 PRINT"{CLR,SPACE}";BE$ 51070 PO=1 51080 FOR I=1 TOPO:PRINT" ";:NEXT I:PRINTVG$ 51085 IF VG$=MID$ (BE$,PO,LEN (VG$)) THENPRINT"GEFUNDEN !":END 51090 FOR I=LEN (VG$) TO1STEP -1 51095 :IF MID$ (BE$,PO+I,1)="" THENPRINT"NICHT GEFUNDEN!":END 51105 :IF MID$ (VG$,I,1)=MID$ (BE$,PO+I-1,1) THEN51120 51109 :IF ASC (MID$ (BE$,PO+I-1,1))=32 THENPO=PO+LEN (VG$):I=0:GOTO 51120 51110 :PO=PO+M%(ASC (MID$ (BE$,PO+I-1,1))-64)+LEN (VG$)-I:I=0 51120 NEXT I 51130 GOTO 51080
Wie funktioniert das in Bild 5? Nun, der Suchvorgang beginnt ganz klar am Anfang des Befehlssatzes. Besteht hier noch keine Übereinstimmung, so wird VGS einfach um eine bestimmte Zeichenanzahl nach rechts verschoben. Woher weiß das Programm, um wie viele Zeichen es bei Nichtübereinstimmung verschieben muß? Das Prinzip ist eigentlich ganz einfach: Zunächst wird eine Tabelle angelegt, aus der hervorgeht, wie viele Zeichen jeder Buchstabe im Suchwort VG$ entfernt ist. Bei unserem Suchwort VG$=»NIMM« gilt folgendes:
M = 0 entfernen. Bei doppelt auftretenden Buchstaben wie zum Beispiel M im Wort NIMM, wird der Buchstabe notiert, der dem Ende des Wortes näher ist. Für alle Buchstaben, die im Suchwort VG$ nicht enthalten sind, wird als »Entfernungswert« die Länge von VG$ eingetragen – also 4 bei VG$=»NIMM«.
| VG$ | = NIMM | ||||
| A | = 4 | J | = 4 | S | = 4 |
| B | = 4 | K | = 4 | T | = 4 |
| C | = 4 | L | = 4 | U | = 4 |
| D | = 4 | M | = 0 | V | = 4 |
| E | = 4 | N | = 3 | W | = 4 |
| F | = 4 | O | = 4 | X | = 4 |
| G | = 4 | P | = 4 | Y | = 4 |
| H | = 4 | Q | = 4 | Z | = 4 |
| I | = 2 | R | = 4 | ||
Das Erstellen dieser Tabelle findet beim Programm in den Zeilen 51020-51052 statt. Aus dieser Tabelle kann nun immer ermittelt werden, um wie viele Zeichen VG$ nach einem mißglückten Versuch nach rechts verschoben werden darf. Und das geht so: Betrachten Sie bitte wieder Bild 5. VG$ wird von hinten nach vorn beziehungsweise von rechts nach links durchlaufen. Dabei wird jedes Zeichen mit der entsprechenden Stelle in BE$ verglichen. Stimmen alle Zeichen überein, so ist das Word VG$ in BE$ gefunden. Erfolgt jedoch bei einem Buchstabenvergleich keine Übereinstimmung, so darf BG$ nach rechts verschoben werden – und zwar genau um die Zeichenzahl beziehungsweise zu dem Wert, der gemäß der Tabelle dem Zeichen von BE$ zugeordnet ist, bei dem zum ersten Mal keine Übereinstimmung festgestellt wurde. Beim ersten Suchschritt wird NIMM mit OTTO verglichen. Es wird also schon beim letzten Buchstaben Unübereinstimmung festgestellt, daß M ungleich O ist. Gemäß der Tabelle hat O den Wert 4. VG$ darf somit um 4 Zeichen nach rechts verschoben werden. Allerdings spielt der Tabellenwert nicht die einzige Rolle beim Verschieben. Vom aus der Tabelle ermittelten Wert wird noch die Entfernung des Buchstabens vom Wortende abgezogen, bei der die erste Nichtübereinstimmung gefunden wurde. Dies ist nicht ganz einfach verständlich. Ich rate Ihnen daher eine eigenständige Analyse von Listing 3, Zeile 51060-51130. Danach wird Ihnen das Prinzip dieser Methode mit Sicherheit klar sein.
Man kann nicht so ohne weiteres behaupten, daß die zweite Methode der ersten überlegen ist. Auf den ersten Blick scheint Methode 2 zwar schneller zu sein, da weniger Verschiebungen notwendig sind. Dem steht jedoch eine wesentlich größere Rechenzeit als bei Methode 1 gegenüber. Am besten, Sie probieren selbst einmal aus, mittels TI$-Zeitmessung, welche der beiden Methoden die geeignetere ist. Auf jeden Fall ist die Methode 2 äußerst interessant, nicht zuletzt deshalb, weil sie erst vor kurzem entdeckt wurde.
Möglichkeit 3
Wie Methode 1 eignet sich Methode 2 jedoch kaum zur Adventureprogrammierung, sondern vielmehr zur Entwicklung von ELIZA-ähnlichen Simulationen. Anders Methode 3:
Die 3. Methode entspricht weitgehend der Methode, die ich bereits in meinem ersten Adventure-Kurs verwendet habe. Es geht darum, den eingegebenen Befehlssatz BE$ zunächst in einzelne Worte BE$(0)-BE$(N) zu zerlegen, wobei N für die Anzahl der im Befehlssatz vorkommenden Worte steht. Der Befehlssatz kann in BE$(0)="NIMM" BE$(1)="DAS" und BE$(2)="SCHWERT" zerlegt werden.
BE$(3) ist hier natürlich " ", wodurch das Satzende gekennzeichnet ist.
Nun wurde eine Wortzählvariable WZ auf 0 gesetzt. Sodann wurde das Wort BE$(WZ) codiert – also im Wortschatz gesucht. Es wird so eine Verbzahl beziehungsweise Objektzahl etc. ermittelt. Wird das Wort nicht im Wortschatz gefunden, so kommt die Fehlermeldung »ICH KENNE DAS WORT...NICHT«, und der gesamte Codiervorgang wurde abgebrochen. Sodann wurde der Wortzähler WZ um 1 erhöht, und eine neue Suchrunde begann. Der ganze Satz wurde so also codiert. Der Suchvorgang wurde dann abgeschlossen, wenn BE$(WZ)=» « war (also Satzende) oder BE$(WZ)=»UND« beziehungsweise »,« war (=Befehlssatz mit mehreren Befehlen). Wurde der Suchvorgang wegen letzterem unterbrochen, so wurde die Variable UD auf den Wert 1 gesetzt. Dadurch wußte das Programm, daß beim nächsten Mal, wenn der Spieler dran ist, die Befehlseingabe (INPUT-ROUTINE) übersprungen werden muß und die Codierung im alten Befehlssatz dort fortgesetzt werden mußte, wo man zuletzt wegen einem UND beziehungsweise KOMMA abgebrochen hatte.
In Bild 6a und 6b finden Sie nochmals die beiden Ablaufschemen, die diese Codiermethode beschreiben:


Wer meinen ersten Adventurekurs durchgearbeitet hat, dem sind diese beiden Ablaufschemen mit Sicherheit gut bekannt.
Obwohl die Ablaufschemen bereits über 1 Jahr alt sind und es inzwischen gravierende Verbesserungen im Bereich der Adventureprogrammierung gegeben hat (wie Sie schon bald sehen werden !), haben sie keineswegs ihre Gültigkeit verloren.
Bild 6a stellt das Gesamtablaufschema eine Adventures dar. Sie können jedes Adventure-Programmierkonzept in dieses Schema quetschen. Unter Adventure läßt sich natürlich auch jedes andere KI-Programm verstehen, bei dem im Programm folgende Teile enthalten sind: Befehlseingaberoutine, Analyse des Befehlssatzes anhand eines gegebenen Wortschatzes, Interpretation der Analyse beziehungsweise Reaktion auf die Befehlseingabe.
Bild 6b beschreibt den Ablauf der Befehlscodierung nach Methode 3. Lassen Sie uns jetzt Methode 3 programmieren.
Ergänzen Sie dieses Mal Listing 1 bitte um Listing 4. Unser Parsersystem 4.0 ist nun insofern komplett, daß nach der Befehlseingabe der Befehlssatz BE$ in einzelne Worte BE$(1)-BE$(N) zerlegt wird. Dabei ist festgelegt, daß der Befehlssatz aus maximal 10 einzelnen Wörtern bestehen darf. Dies läßt sich natürlich leicht abändern, aber 10 sollen uns zunächst genug sein zum Experimentieren. Die Wortzerlegung funktioniert äußerst primitiv: Es wird einfach BE$ durchlaufen und nach den SPACEs zwischen den einzelnen Wörtern gesucht. Eine Dokumentation zu Listing 4 erübrigt sich (hoffe ich doch !!!).
51000 rem befehlszerlegung-codierung ------------------------------------------- 51001 : 51005 rem be$ in be$(1)-be$(n) zerlegen 51010 fori=0to10:be$(i)="":nexti 51020 wz=0 51030 fori=1tolen(be$) 51040 : ifmid$(be$,i,1)=" "thenwz=wz+1:goto51060 51045 : ifwz>10thenprint"eingabe ist zu lang !":i=len(be$)+1:goto51060 51050 : be$(wz)=be$(wz)+mid$(be$,i,1) 51060 nexti
51000 REM BEFEHLSZERLEGUNG-CODIERUNG ------------------------------------------- 51001 : 51005 REM BE$ IN BE$(1)-BE$(N) ZERLEGEN 51010 FOR I=0 TO10:BE$(I)="":NEXT I 51020 WZ=0 51030 FOR I=1 TOLEN (BE$) 51040 : IF MID$ (BE$,I,1)=" " THENWZ=WZ+1:GOTO 51060 51045 : IF WZ>10 THENPRINT"EINGABE IST ZU LANG !":I=LEN (BE$)+1:GOTO 51060 51050 : BE$(WZ)=BE$(WZ)+MID$ (BE$,I,1) 51060 NEXT I
Jetzt muß der Codiervorgang gemäß Bild 6b programmiert werden. Ergänzen Sie das jetzige Programm bitte um Listing 5.
51100 rem be$(1)-be$(n) codieren 51110 wz=0 :rem wortzaehler auf 0 51120 ve=0:ob=0:rem alte erg. loeschen 51130 rem wort be$(wz) im wortschatz suchen 51140 : 51150 : 51160 : 51170 : 51180 : 51190 : 51200 wz=wz+1 51220 ifbe$(wz)=""then52000 51230 goto51130 52000 rem reaktion auf befehl --------------------------------------------------
51100 REM BE$(1)-BE$(N) CODIEREN 51110 WZ=0 :REM WORTZAEHLER AUF 0 51120 VE=0:OB=0:REM ALTE ERG. LOESCHEN 51130 REM WORT BE$(WZ) IM WORTSCHATZ SUCHEN 51140 : 51150 : 51160 : 51170 : 51180 : 51190 : 51200 WZ=WZ+1 51220 IF BE$(WZ)="" THEN52000 51230 GOTO 51130 52000 REM REAKTION AUF BEFEHL --------------------------------------------------
Auch Listing 5 läßt sich leicht verstehen. Auf Einbau von UND-Befehlstrennungen habe ich der Einfachheit halber zunächst verzichtet. Listing 5 bringt im Moment allerdings noch gar nichts. Wir müssen uns zunächst noch überlegen, wie wir das Wort in unserer relativen Wortschatzdatei finden beziehungsweise die einzelnen Befehlswörter BE$(1)-BE$(N). Im letzten Adventurekurs war dies noch ganz einfach: Da gab es für Verben und Objekte einzelne Tabellen (dimensionierte Stringfelder), in denen gesucht wurde. Jetzt befindet sich der gesamte Wortschatz, also Verben, Objekte, unwichtige Wörter, Richtungen etc; in einer relativen Datei, die alphabetisch sortiert ist. Nun, wie findet man Wörter in einer sortierten relativen Datei am schnellsten ? Ganz klar – binäre Suchmethode. Wir müssen also in Listing 5 eine binäre Suchroutine einbauen. Eine solche Routine haben wir ja bereits im Kursabschnitt »Dateiverwaltung« kennengelernt. Bevor wir die binäre Suchroutine in Listing 5 einbauen, müssen wir uns allerdings noch einige Gedanken zur Organisation des Wortschatzes machen.
Kleine Wortschatzlehre
Werfen Sie an dieser Stelle bitte nochmals einen Blick auf Bild 3. Dort habe ich Ihnen gezeigt, wie sich die 25 Zeichen eines jeden Satzes der Wortschatz-Datei aufteilen: Die ersten 21 Zeichen sind für das Wort selbst reserviert. Also zum Beispiel für NIMM, ÖFFNE (Verben) oder auch HAUSTÜR, SCHRANK (Objekte). Die letzten 4 Zeichen jedes Satzes sind für eine Zahl – die Codierungszahl – reserviert. Was hat es nun mit dieser Codierungszahl auf sich ? Ganz einfach, aus ihr läßt sich ermitteln, welcher Wortart das entsprechende Wort angehört und welche Zahl ihm als Codierung (zum Beispiel Verbzahl VE usw.) zugedacht ist. Die 4stellige Zahl ist also vielmehr als zwei Zahlen beziehungsweise eine Ziffer und eine Zahl zu verstehen, die unmittelbar nebeneinander stehen. Bild 7 macht die genaue Unterteilung deutlich:

Die erste Ziffer der 4stelligen Zahl beschreibt somit die Wortart WA, während die letzten drei Ziffern den Wortcode WC bestimmen. Wir können also festlegen:
Wortart: WA= VAL(MID$(A$,22,1))
Wortcode: WC= VAL(MID$(A$,23,3))
A$ steht dabei für einen beliebigen Satz der Wortschatz-Datei. Das tatsächliche Wort ermittelt sich als
A$=LEFT$(A$,21)
Allerdings hat das absolute Wort nicht immer die Länge von 21 Zeichen. Es wäre jedoch günstig, wenn in A$ nur das Wort (keine SPACEs mehr danach) stehen würde. Die SPACEs kann man ganz einfach eliminieren:
9340 IFRIGHT$(A$,1)="cursor right" THENA$=LEFT$(A$,LEN(A$-1)) :GOTO 9340
Diese Zeile wiederholt sich so lange, bis in A$ nur noch das Wort und keine SPACEs mehr danach stehen. Eigentlich darf ich hier gar nicht von SPACEs reden. Was wir auf dem Bildschirm als SPACE sehen, ist bei Sätzen, die von relativen Dateien, die mit unserem Relative-Dateien-Editor erstellt wurden, stammen, tatsächlich ein »cursor right«-Zeichen. Achten Sie also stets darauf, daß Sie in Sätzen aus unseren Dateien niemals nach einem SPACE, sondern vielmehr nach dem cursor-right-Zeichen suchen (vergleiche Beschreibung zum Relative-Dateien-Editor im Kapitel »relative Dateien«)!
Natürlich muß jetzt noch festgelegt werden, welche Wortartzahl welcher Wortart entspricht:
| 1 | = Verb |
| 2 | = Richtung |
| 3 | = Objekt |
| 4 | = Aus-Wort |
Bisher treten in unserem Wortschatz nur 1 und 2 als WA-Werte auf – kein Wunder, denn unser Ausgangs-Wortschatz besteht ja bisher nur aus Verben und Richtungen (die im Prinzip auch als Verben verstanden werden können, jedoch trotzdem einen eigenen WA-Wert benötigen, wie Sie gleich sehen werden).
Unter Aus-Wort verstehe ich jedes Wort, das im Befehlssatz auftreten kann, bei der Codierung jedoch keinerlei Rolle spielt. Gemeint sind Wörter wie zum Beispiel der, die, das, nach, vom und so weiter.
Bei dem Wortschatz, den wir im letzten Adventurekurs verwendet haben, mußten die Wörter nicht mit Zahlen gekennzeichnet werden. Auch Wörter mit gleicher Bedeutung wurden anders abgehandelt. Die nun angewandte Methode hat natürlich ihre Vorteile: Zum einen läßt sich der Wortschatz leichter um Worte erweitern, und zum anderen müssen nicht mehr mehrere Einzeltabellen verwaltet werden (für die verschiedenen Wortarten).
Eine gute Wortschatzverwaltung beeinflußt nicht zuletzt auch die Anwenderfreundlichkeit eines Parsers in positivem Maße. Ein anwendungsfreundlicher Parser ist ein Parser, der bei der Befehlssatz-Analyse äußerst tolerant vorgeht.
Es muß zum Beispiel anstelle des Verbs NIMM auch NEHME und NEHM sowie gegebenenfalls auch HOLE und ERGREIFE verstehen. Der Trick bei dieser Sache ist, daß mehr Worte verstanden werden, als eigentlich im Wortschatz stehen. Was das soll? Ganz einfach:
Nehmen wir einmal an, wir wollen in einem Adventure die NIMM-Routine programmieren:
IF VERB=NIMM AND OBJEKT X IST HIER THEN PRINT "OK. ICH HABE OBJEKT X."
So könnte eine vereinfachte Syntax lauten. Dem Verb NIMM ist gemäß der Wortschatztabelle der Wortwert 12 zugeordnet (also VE=12). Wir können also sagen:
IF VE=12 AND OBJEKT X IST HIER THEN PRINT "OK. ICH HABE OBJEKT X".
Was passiert, wenn der Spieler den Befehlssatz »HOLE OBJEKT X« eingibt? Nun, das Programm antwortet mit »OK. ICH....«, da dem Verb HOLE ebenfalls der Wortwert (beziehungsweise Verbwert) 12 zugeordnet ist. Unser Wortschatz ist ziemlich intelligent aufgebaut – intelligent insofern, daß Verben gleicher Bedeutung der gleiche Verbwert zugeordnet ist. Ein guter Parser sollte allerdings »NIMM DAS SCHWERT« genauso gut verstehen wie »ICH WILL DAS SCHWERT NEHMEN« und »NEHM DAS SCHWERT« (auch wenn es grammatikalisch nicht in Ordnung ist). Wie stellt man es nun an, daß NEHM genauso gut verstanden wird wie NEHMEN? Nun, ganz einfach. NEHM steckt schließlich in NEHMEN drin. Nicht zuletzt ist dies auch der Grund, warum im Wortschatz immer die längstmögliche Form eines Verbs gespeichert wird – insofern ist dies möglich. Die meisten routinierten Adventure-Spieler geben eh in der Sytax NIMM, VERLIER, GIB etc. ein. Wir können uns deshalb auch hauptsächlich an dieser Form orientieren. Nehmen wir einmal an, der Satz »NIMM DAS SCHWERT« soll codiert werden (welcher sonst). BE$(0) ist dann ganz klar »NIMM«. Wir wollen BE$(0) jetzt anhand des Wortschatzes codieren. Mittels der binären Suchmethode werden dann verschiedene Sätze vom Wortschatz eingelesen (als String A$) und mit SU$(=BE$(0)) verglichen. Bild 8 zeigt, wie das Wort NIMM im Wortschatz nach der binären Suchmethode gefunden wird.

Wie wird A$ eingentlich mit SU$ verglichen?
Ein Vergleich IF A$=SU$ THEN PRINT »GEFUNDEN« wäre mit Sicherheit falsch, da man in diesem Fall niemals das Verb NEHM finden würde (im Wortschatz steht nur NEHME). Der korrekte Vergleich lautet daher:
IF SU$=LEFT$(A$,LEN(SU$)) THEN ? "GEFUNDEN"
Dadurch sind bei der Befehlseingabe auch Abkürzungen der Verben erlaubt – zum Beispiel UNTERSU statt UNTERSUCHEN beziehungsweise NEHM statt NEHMEN. Dies ist mit Sicherheit jedem sonnenklar. Was aber, wenn der Spieler nach Norden gehen will? Er wird dann mit größter Wahrscheinlichkeit BE$=»N« eingeben. Aufgrund unserer bisherigen Abfragemethode IF SU$=LEFT$(A$,...) wird das Programm annehmen, daß das erste Wort, das mit N beginnt, das richtige ist. Werfen Sie doch bitte einmal einen Blick in die Wortschatztabelle: Sie werden merken, daß dort für Norden sowohl »N« als auch »NORDEN« vermerkt ist. Nicht umsonst haben Richtungen deshalb einen anderen WA-Wert als Verben. Sicher erinnern Sie sich daran, daß A$ vor dem Vergleich mit SU$ präpariert wird. Zunächst wird A$ mit einem Satz der Wortschatzdatei gefüllt. Dann werden Wortcode WC und Wortart WA ermittelt. Zuletzt steht in A$ dann nur noch das absolute Wort. Dadurch, daß die Wortart beim Vergleichen bereits bekannt ist, kann folgender Vergleich programmiert werden:
IF WA=1 AND SU$=LEFT$(A$,LEN(SU$)) THEN ? "VERB GEFUNDEN"
IF WA=2 AND SU$=A$ THEN ? "RICHTUNG GEFUNDEN"
Sie sehen also, bei RICHTUNGEN muß SU$ mit A$ übereinstimmen und SU$ nicht nur linksbündig in A$ enthalten sein.
Wie muß nach Objekten gesucht werden? Wir wollen uns auch mit dieser Frage beschäftigen – auch wenn sich im Wortschatz bisher noch keine Objekte befinden.
Objektnamen setzen sich oft aus Hauptwörtern zusammen. So zum Beispiel HAUSTÜR, HOLZKISTE, WEINFLASCHE und so weiter. Will der Spieler eine Haustür öffnen, so kann er sagen »ÖFFNE DIE HAUSTÜR« oder auch »ÖFFNE DIE TÜR«. Der Parser muß also wissen, daß HAUSTÜR=TÜR, HOLZKISTE=KISTE und so weiter ist. Entscheidend zur Bestimmung eines Objektes ist stets das zweite Hauptwort (beziehungsweise das Hauptwort, das ganz rechts im Objektnamen steht, beziehungsweise das Objekt selbst, wenn es sich nicht aus mehreren Hauptwörtern zusammensetzt). Es bietet sich also an, bei der Objektsuche von »rechts her« zu vergleichen:
IF WA=3 AND SU$=RIGHT$(A$,LEN(SU$)) THEN ? "OBJEKT-GEFUNDEN"
Das war wieder mal ziemlich viel Theorie. Listing 6 setzt das eben Gelernte in die Praxis um.
Dokumentation:
| 10-30 | Hier wird die Wortschatzdatei geöffnet. Vor Programmstart ist es also erforderlich, daß Sie die Diskette mit der Wortschatzdatei einlegen. Anschließend wird ein Suchbegriff SUS eingegeben. |
| 9000-9230 | Hier stehen die drei Unterprogramme, die beim Arbeiten mit relativen Dateien immer wieder benötigt werden: Positionieren, Lesen und Schreiben. |
| 9300-9350 | Hier wird ein Satz A$ der Wortschatzdatei gelesen. Zunächst werden Wortcode WC und Wortart WA gelesen, dann die Codierzahl vom A$ abgeschnitten und alle SPACES (cursor-rights) hinter dem absoluten Wort weggenommen (Zeile 9340). |
| 9500-9780 | Hier steht die binäre Suchroutine, wie ich sie Ihnen bereits im Kapitel Dateiverwaltung Listing 15 beschrieben habe. Die Routine wurde zwecks Zusammenarbeit mit relativer Datei entsprechend modifiziert. |
| 9540 | Für SZ muß die Anzahl der Worte, die im Wortschatz enthalten sind, eingesetzt werden. (Im Moment haben wir 197 Worte.) |
| 9620-9637 | Hier werden die Objekte, Verben und Richtungen gesucht. |
| 9623 | Wenn die Zeichenanzahl des Suchbegriffs weniger als 3 Zeichen beträgt, muß davon ausgegangen werden, daß es sich bei SU$ um eine Richtungsangabe handelt. Das heißt auch, daß Verbenabkürzungen mindestens 3 Buchstaben betragen müssen. Man kann für INVENTUR also nicht die Abkürzung I eingeben- es sei denn, I wurde zusätzlich im Wortschatz definiert (Zeile 9637 sorgt dafür, daß solche Verbabkürzungen akzeptiert werden). Ist LEN(SU$) also kleiner als 3, so werden die Zeilen 9625 (Verbvergleich linksbündig) und Zeile 9630 (Objektvergleich rechtsbündig) übersprungen. |
| 9625 | Verbsuche (linksbündig) |
| 9630 | Objektsuche (rechtsbündig) |
| 9635 | Richtungssuche (absolut) |
| 9637 | Verbsuche (absolut) – für Verben, deren Abkürzung auch weniger als 3 Zeichen betragen darf – müssen im Wortschatz extra verzeichnet werden! |
10 open1,8,3,"wortschatz":open15,8,15:rem wortschatz-datei oeffnen
20 inputsu$
30 goto9500
9000 rem zeiger positionieren --------------------------------------------------
9001 :
9010 hb%=sa/256:lb%=sa-hb%*256
9020 print#15,"p";chr$(3);chr$(lb%);chr$(hb%);chr$(1)
9030 input#15,a:ifa=50thenprint"satznummer zu hoch"
9040 return
9100 rem satz lesen ------------------------------------------------------------
9101 :
9110 ifa=50thenreturn
9120 input#1,a$
9130 return
9200 rem satz schreiben --------------------------------------------------------
9201 :
9210 ifa=50thenreturn
9220 print#1,a$
9230 return
9300 rem satz-teilbereich lesen ------------------------------------------------
9301 :
9310 ifa=50thenreturn
9320 input#1,a$
9330 wa=val(mid$(a$,22,1)):rem wortart
9332 wc=val(mid$(a$,23,3)):rem wortcode
9335 a$=left$(a$,21) :rem wort
9340 ifright$(a$,1)="{rght}"thena$=left$(a$,len(a$)-1):goto9340
9345 printa$,len(a$)
9350 return
9500 rem binaere suchroutine ---------------------------------------------------
9520 rem su$=suchwort
9540 sz=197:n=int(log(sz)/log(2))+1 :rem maximal-formel
9560 sa=(2^n)/2:rem mitte der ges.dat
9565 gosub9000:gosub9300 :rem a$ lesen
9580 n=n-1 :rem 1.abfrage
9600 :
9620 rem su$ mit a$ vergleichen ------
9623 : iflen(su$)<3then9635
9625 : ifwa=1 and su$=left$(a$,len(su$)) thenprint"su$=verb:satz ";sa:end
9630 : ifwa=3 and su$=right$(a$,len(su$))thenprint"su$=objekt:satz ";sa:end
9635 : ifwa=2 and su$=a$ thenprint"su$=richtung:satz ";sa:end
9637 : ifwa=1 and su$=a$ thenprint"su$=verb:satz ";sa:end
9640 : ifsu$<a$thensa=sa-(2^(n-1)):gosub9000:gosub9300:goto9700
9660 : ifsa+(2^(n-1))>szandn>-1thenn=n-1:goto9660 :rem ''ueberlauf''
9680 : sa=sa+(2^(n-1)):gosub9000:gosub9300 :rem ''halbieren''
9700 :
9720 :n=n-1 :rem naechst. halbieren
9740 :ifn<0thenprint"su$ gibt's nicht! in 1220":end :rem su$ nicht in datei
9760 print"sa=";sa;" n=";n :rem unnoetig !!
9780 :goto9600:rem naechst. such-vers.
10 OPEN 1,8,3,"WORTSCHATZ":OPEN 15,8,15:REM WORTSCHATZ-DATEI OEFFNEN 20 INPUTSU$ 30 GOTO 9500 9000 REM ZEIGER POSITIONIEREN -------------------------------------------------- 9001 : 9010 HB%=SA/256:LB%=SA-HB%*256 9020 PRINT#15,"P";CHR$ (3);CHR$ (LB%);CHR$ (HB%);CHR$ (1) 9030 INPUT#15,A:IF A=50 THENPRINT"SATZNUMMER ZU HOCH" 9040 RETURN 9100 REM SATZ LESEN ------------------------------------------------------------ 9101 : 9110 IF A=50 THENRETURN 9120 INPUT#1,A$ 9130 RETURN 9200 REM SATZ SCHREIBEN -------------------------------------------------------- 9201 : 9210 IF A=50 THENRETURN 9220 PRINT#1,A$ 9230 RETURN 9300 REM SATZ-TEILBEREICH LESEN ------------------------------------------------ 9301 : 9310 IF A=50 THENRETURN 9320 INPUT#1,A$ 9330 WA=VAL (MID$ (A$,22,1)):REM WORTART 9332 WC=VAL (MID$ (A$,23,3)):REM WORTCODE 9335 A$=LEFT$ (A$,21) :REM WORT 9340 IF RIGHT$ (A$,1)="{RIGHT}" THENA$=LEFT$ (A$,LEN (A$)-1):GOTO 9340 9345 PRINTA$,LEN (A$) 9350 RETURN 9500 REM BINAERE SUCHROUTINE --------------------------------------------------- 9520 REM SU$=SUCHWORT 9540 SZ=197:N=INT (LOG (SZ)/LOG (2))+1 :REM MAXIMAL-FORMEL 9560 SA=(2^N)/2:REM MITTE DER GES.DAT 9565 GOSUB 9000:GOSUB 9300 :REM A$ LESEN 9580 N=N-1 :REM 1.ABFRAGE 9600 : 9620 REM SU$ MIT A$ VERGLEICHEN ------ 9623 : IF LEN (SU$)<3 THEN9635 9625 : IF WA=1 AND SU$=LEFT$ (A$,LEN (SU$)) THENPRINT"SU$=VERB:SATZ ";SA:END 9630 : IF WA=3 AND SU$=RIGHT$ (A$,LEN (SU$)) THENPRINT"SU$=OBJEKT:SATZ ";SA:END 9635 : IF WA=2 AND SU$=A$ THENPRINT"SU$=RICHTUNG:SATZ ";SA:END 9637 : IF WA=1 AND SU$=A$ THENPRINT"SU$=VERB:SATZ ";SA:END 9640 : IF SU$<A$ THENSA=SA-(2^(N-1)):GOSUB 9000:GOSUB 9300:GOTO 9700 9660 : IF SA+(2^(N-1))>SZAND N>-1 THENN=N-1:GOTO 9660 :REM ''UEBERLAUF'' 9680 : SA=SA+(2^(N-1)):GOSUB 9000:GOSUB 9300 :REM ''HALBIEREN'' 9700 : 9720 :N=N-1 :REM NAECHST. HALBIEREN 9740 :IF N<0 THENPRINT"SU$ GIBT'S NICHT! IN 1220":END :REM SU$ NICHT IN DATEI 9760 PRINT"SA=";SA;" N=";N :REM UNNOETIG !! 9780 :GOTO 9600:REM NAECHST. SUCH-VERS.
Zum Abschluß von Methode 3 müßte Listing 6 nun eigentlich mit Listing 5 kombiniert werden. Ich möchte jedoch davon absehen, da Sie dies zum einen leicht selbst machen können und es zum anderen noch eine bessere Möglichkeit gibt, die im Prinzip eine Verbesserung des letzten Programms bedeutet:
Möglichkeit 4
Unser Wortschatz besteht aus 197 Worten. Um ein bestimmtes Wort mittels der binären Suchmethode in diesem Wortschatz zu finden, beziehungsweise um festzustellen, daß dieses Wort im Wortschatz nicht vorkommt, sind maximal 8 Lesezugriffe auf Disk erforderlich. Jeder Schreib- beziehungsweise Lesezugriff auf Disk kostet Zeit. Auch durch Assemblerprogrammierung läßt sich diese Zugriffszeit kaum verkürzen. Es gibt allerdings eine Suchmethode, bei der weniger Lesezugriffe aus Disk nötig sind als bei Möglichkeit 3.
Das Index-Binäre-Suchprinzip
Keine Angst, dieses Prinzip läßt sich einfacher verstehen, als der komplexe Name vermuten läßt. Beim Index-Binären-Suchprinzip handelt es sich lediglich um eine Verbesserung der binären Suchmethode. Gehen wir einmal davon aus, daß unser Suchbegriff SU$=»NIMM« ist. Unser Parser erkennt nicht auf Anhieb, daß SU$ ein VERB ist. Er erkennt SU$ erst dann als Verb an, wenn er es im Wortschatz gefunden hat. Etwas könnte der Parser allerdings schon vor Beginn des Suchvorgangs wissen – und zwar, daß der Suchbegriff mit dem Buchstaben N beginnt. Wozu also den gesamten Wortschatz durchsuchen, der ja eh alphabetisch geordnet ist? Es würde doch vollkommen ausreichen, den Bereich, in dem alle mit N beginnenden Wörter stehen, mittels der binären Suchmethode zu durchsuchen! Dazu benötigt man zunächst einen Index – eine Tabelle, in der steht, ab welchem Satz die mit A beginnenden Wörter stehen, ab welchem die mit B beginnenden, und so weiter. Das Programm in Listing 7 ist in der Lage, den Index zu einem beliebigen Bereich einer relativen Datei (der natürlich alphabetisch geordnet sein muß) zu erstellen.
Das Programm ist anwenderfreundlich – es erkennt unlogische Eingaben und erklärt sich nach Programmstart selbst.
Es lohnt sich eigentlich gar nicht, den Index-File jedesmal erst lange von Disk zu laden. Es ist günstiger, die Werte einfach in DATA-Zeilen im Listing abzulegen.
Durch Anwendung der Index-Binären-Methode sind nur wenig Lesezugriffe auf Disk notwendig, bis ein Wort gefunden ist: Die größte Wortgruppe unserer Wortschatzdatei stellen die Wörter dar, die mit S beginnen: Das sind 33 Worte. Im ungünstigsten Fall werden nur 6 Zugriffe benötigt, um ein Wort zu finden. Vorteilhaft an der neuen Methode ist außerdem, daß Wörter, die mit einem Buchstaben beginnen, der im Wortschatz als Anfangsbuchstabe überhaupt nicht auftaucht (zum Beispiel C,J,M,X und Y), erst gar nicht gesucht werden, weil von vorneherein klar ist, daß sie im Wortschatz nicht auftreten.
Bei Methode 4 wird allerdings auch noch etwas anderes verbessert: Das Zerlegen von BE$ in BE$(0)-BE$(N). Eigentlich ist es nämlich Quatsch, BE$ in einzelne Worte zu zerlegen. Es reicht völlig aus, mit Zeigern zu arbeiten. Bild 9 verdeutlicht dies.

Das mit den Zeigern funktioniert ganz einfach: Zu Beginn wird Zeiger A auf das erste Zeichen von BE$ gesetzt. Zeiger B läuft von Zeiger A aus nach rechts bis zum nächsten Space. Das erste Suchwort ist somit festgelegt. Nachdem dieses Wort codiert ist, wird Zeiger A auf das Zeichen rechts neben Zeiger B gesetzt – auf das erste Zeichen des neuen Suchwortes. Jetzt läuft Zeiger B wieder nach rechts und sucht erneut das Wortende, und so weiter.
Trifft Zeiger B auf kein Zeichen mehr, so ist er am Ende des Satzes angelangt – der Satz ist komplett codiert.
Damit das letzte Wort der Sätze noch codiert wird, muß am Befehlssatz BE$ noch ein SPACE angehängt werden.
Bei Methode 4 soll auch noch etwas anderes berücksichtigt werden – das Satzende. Es gibt 3 Möglichkeiten, einen Satz zu beenden – mit einem Punkt, einem Ausrufezeichen, einem Fragezeichen oder einfach nichts.
Uns interessieren allerdings nur zwei Fälle: Fragezeichen oder nicht. Bevor BE$ also codiert wird, muß künftig darauf geachtet werden, ob am Ende des Satzes ein Satzzeichen steht. Die Variable SN speichert das Satzzeichen (Punkt etc. =0, Fragezeichen =1). Dann wird BE$ so gekürzt, daß das Zeichen wegfällt.
Nach dem Codiervorgang müssen folgende Werte in Form von Variablen vorliegen:
| SN | Satzzeichen am Satzende |
| VE | Verbzahl |
| O | Objektzahl für erstes Objekt |
| O2 | Objektzahl für zweites Objekt (falls vorhanden) |
| RI | Richtung |
| UD | UND-Speicher |
| AD | Adjektiv-Zahl |
Zur Objektcodierung werden zwei Variablen O1 und O2 benötigt, da ein Befehl aus maximal zwei Objekten (direktes und indirektes) bestehen kann. Beispiel: »ÖFFNE DIE TÜR MIT DEM SCHLÜSSEL«. Im Befehl kann auch nur ein Verb auftreten. Gegebenenfalls kann auch noch eine Variable AD für Adjektiv zur näheren Beschreibung der Objekte verwendet werden. Adjektive haben die Wortzahl WZ=5.
Jetzt kann Methode 4 wie in Listing 8 programmiert werden:
Die REM-Zeilen können beim Abtippen nicht so ohne weiteres weggelassen werden!
Mit REM-Zeilen beansprucht Parser 4.0 zirka 3 KByte RAM Basic-Speicher. Entfernt man die REM-Zeilen unter Berücksichtigung der dann teils veränderten Ansprungsadressen, so werden nur zirka 2 KByte Basic-RAM benötigt.
Parser 4.0 ist ziemlich intelligent und zum größten Teil sogar idiotensicher. Das heißt, es werden die blödsinnigsten Befehlseingaben akzeptiert. Zum Beispiel:
"NIMM DAS SCHWERT"
" NIMM DAS SCHWERT!"
"NIMM SCHWERT, UNTERSUCHE ES"
Die UND-Routine ist auch eingebaut. Außerdem speichert Parser 4.0 jeweils das zuletzt eingegebene Objekt in der Variablen OM. Bei Eingabe eines der Schlüsselwörter IHN, SIE oder ES (zum Beispiel BE$=»UNTERSUCHE ES«), so wird O gleich OM gesetzt. Der Clou ist auch, daß Sie einfach nur GOSUB 50000 eingeben müssen, wenn vom Spieler eine Befehlseingabe erwartet wird. Parser 4.0 erkennt automatisch, ob die INPUT-Routine angesprungen werden muß oder ob der letzte Befehlssatz noch nicht ganz abgehandelt ist (UND-FALL). Kann Parser 4.0 ein Wort nicht codieren, weil es im Wortschatz nicht vorkommt, so erfolgt »ICH KENNE DAS WORT .... NICHT«, und anschließend wird zur Befehlseingabe-Routine zurückgesprungen. Dies ist notwendig, damit ein Spieler niemals verlieren kann, wenn er bei einer entscheidenden Situation (zum Beispiel Kampf mit Monster) sich aus Versehen vertippt (zum Beispiel TOTE MONSTER statt TOETE MONSTER). Das Programm darf also erst dann weiterlaufen, wenn der Spieler eine korrekte Befehlseingabe gemacht hat.
Nach dem Rücksprung aus Parser 4.0 liegt der Satz codiert in den Variablen SN,VE,O1,O2,UD,AD vor. Natürlich können Sie Parser 4.0 jetzt noch nicht gründlich testen, da im Wortschatz momentan noch die Objekte fehlen. Zunächst rate ich Ihnen, die nun folgende Dokumentation gründlich zu studieren, damit Sie in der Lage sind, Parser 4.0 selbst zu modifizieren – denn dies ist beim Einbau in eigene Programme fast immer notwendig!
Dokumentation zu Parser 4.0
| 10 | Hier wird die Wortschatz-Datei geöffnet. Das heißt es muß die Diskette eingelegt sein, auf der sich der Wortschatz befindet. |
| 20-30 | Hier stehen die DATAs zum Wortschatz-Index, die mit dem Index-Maker ermittelt wurden. |
| 35 | Tatsächlich besteht die Index-Tabelle nicht nur aus 26, sondern 27 Elementen. Das 27. Element steht für den Buchstaben, der nach Z kommt. Richtig, nach Z kommt kein Buchstabe mehr. Das 27. Element der Index-Tabelle errechnet sich vielmehr aus ANZAHL WÖRTER DER WORTTABELLE +1. Dies ist nötig, damit das Programm erkennt, von welchem bis zu welchem Satz die mit Z beginnenden Wörter zu finden sind. Der 27. Wert wird NICHT automatisch vom Index-Maker erstellt. Er muß als »von Hand« berechnet und eingesetzt werden. |
| 40 | Einlesen der Index-Datas in das Index-Feld IN(0)-IN(26) |
| 50-70 | Hier wird Parser 4.0 mittels GOSUB 50000 aufgerufen. Danach werden die codierten Werte, SN, VE, 01, 02, UD, RI, AD ausgegeben. Dann erfolgt Rücksprung zum Parser-Aufruf. Die Zeilen 50-70 dienen also nur zum Testen des Parsers. |
| 50000-50120 | Hier erfolgt die Befehlseingabe in Form einer erweiterten INPUT-Routine, wie ich sie Ihnen im Kursabschnitt Datei-Verwaltung bereits vorgestellt habe. |
| 50012 | Hier wird überprüft, ob die letzte Befehlseingabe bereits voll codiert worden ist oder ob sie nach einem UND abgebrochen wurde. ZB BE$=»NIMM SCHWERT UND GEH NORDEN« wobei erst der Teilbefehl »NIMM SCHWERT« ausgeführt worden ist. Ist der Befehlssatz BE$ also noch nicht komplett codiert, so wird die Befehlseingabe (und natürlich auch die Suche nach dem Satzzeichen, die ja bereits erfolgt ist) übersprungen, alle bisherigen codierten Werte VE,01 etc. werden beibehalten, und es wird dort weiter codiert, wo zuletzt abgebrochen wurde – GOTO 50500. |
| 50400-50450 | Hier wird nach einem Satzzeichen am Ende von BE$ gesucht. |
| 50420 | Alle unnötigen Leerzeichen, die dem Befehlssatz BE$ anhängen, werden weggekürzt. Beispiel: AUS BE$=»NIMM SCHWERT ! « wird BE$=»NIMM SCHWERT!«. |
| 50440-50450 | Wird ein Satzzeichen gefunden, so wird es in der Variablen SN gespeichert (interessant ist dabei eher nur das Fragezeichen!). Dann wird das Satzzeichen weggekürzt. Also aus »NIMM SCHWERT !« wird jetzt »NIMM SCHWERT«. All dies ist notwendig, damit bei einer Befehlseingabe BE$=»NIMM SCHWERT!« nicht die Fehlermeldung » »Ich kenne das Wort SCHWERT!« nicht« nicht auftritt. Denken Sie also stets daran, alle unnötigen Leerzeichen vor oder nach dem Befehl zu eliminieren. Einen Parser zu programmieren, ist wirklich eine verflixte Sache: Es gibt soviele Dinge, die berücksichtigt werden müssen, damit der Parser auch wirklich absturzfrei läuft. Das Wegkürzen der Leerstellen ist ein gutes Beispiel hierfür. Ein Tip: Wenn Sie glauben, den Parser endlich fertig zu haben, dann compilieren Sie ihn mit Hilfe eines Compilers wie zum Beispiel Austro Comp oder Pet Speed. Der Compiler überprüft alle Zeichen und weist auf eventuelle Syntax-Errors hin. Wird ein Parser-Programm fehlerfrei compiliert, so können Sie davon ausgehen, daß im Programm keine schwerwiegenden Fehler vorkommen! |
| 50500-50600 | Hier werden die Zeiger ZA und ZB auf den Befehlssatz BE$ gesetzt oder weitergerückt. |
| 50502 | Wurde beim letzten Vorgang nicht wegen UND abgebrochen, so werden alle bisher codierten Werte auf 0 gesetzt. |
| 50504 | Wird hier festgestellt, daß der letzte Befehlssatz noch nicht komplett codiert worden ist (UD größer 0), so passiert folgendes: Die Richtungszahl RI wird auf 0 gesetzt (warum, erfahren Sie später noch!). Dann erfolgt ein Sprung zu Zeile 50530. Das heißt, alle Operationen oder Einstellungen, die vorgenommen werden, wenn ein neuer Befehlssatz codiert werden soll (also kein wegen UND abgebrochener), werden übersprungen. |
| 50505-50520 | Einstellung der Zeiger auf einen neuen Befehlssatz. |
| 50505 | Alle Leerstellen, die dem eigentlichen Befehl voranstehen, werden entfernt. Beispiel: Aus » NIMM SCHWERT« wird »NIMM SCHWERT«. |
| 50510 | BE$ wird ein Leerzeichen angehängt. Aus BE$ = »NIMM SCHWERT« wird also BE$ = »NIMM SCHWERT «. Dies ist nötig, da später beim Suchvorgang das Auffinden eines Leerzeichens als Markierung eines Wortendes interpretiert wird. |
| 50520 | Zeiger ZA wird auf das erste Zeichen von BE$ gesetzt. |
| 50530 | Zeiger ZB wird auf das Zeichen nach ZA gesetzt. |
| 50540 | Ist MID$(BE$,ZB,1) = "", so wurde das Satzende erreicht. Der Codiervorgang wird mittels RETURN abgebrochen. |
| 50550 | Ist MID$(BE$,ZB,1) kein Leerzeichen, so wird Zeiger ZB um ein Zeichen nach rechts gerückt und es erfolgt ein Rücksprung zu Zeile 50550. Zeiger ZB wird also so lange nach rechts geschoben, bis entweder das Satzende oder ein Leerzeichen gefunden wird. Wird ein Leerzeichen gefunden, so ist das neue Suchwort festgelegt: SU$=MID$(BE$,ZA,ZB-ZA). |
| 50590 | Das neue Suchwort, das von den Zeigern ZA und ZB auf BE$ bestimmt ist, wird im String SU$ abgelegt. |
| 50600-50670 | Es erfolgt eine »Vorbehandlung« des Suchwortes SU$. Hierbei werden solche Wörter gesucht, die Parser-intern gespeichert sind und nicht auf der Wortschatz-relativen Datei verzeichnet sind. |
| 50610 | Hier wird überprüft, ob SU$ gleich UND ist. Wenn ja, so ist klar, daß ein Teilbefehl von BE$ codiert ist und später von hier aus weitercodiert werden muß. Zunächst muß jedoch der bisherig ermittelte Teilbefehl analysiert werden. Dazu wird die Variable UD auf 1 gesetzt und der Codiervorgang mittels RETURN abgebrochen. |
| 50620 | Das gleiche wie bei 50610 in Grün – nur, daß hier nach einem Komma gesucht wird. Die Komma-Suche hat allerdings einen Haken: Das Komma wird nur dann gefunden, wenn links und rechts von ihm mindestens je ein Leerzeichen steht. Bei Befehlssätzen wie zum Beispiel BE$ = »NIMM SCHWERT,BIERFLASCHE« erfolgt eine Fehlermeldung à la »Ich kenne das Wort Bierflasche nicht«. Mit einem direkten Vergleich IF SU$ = "," ist es also nicht abgetan. Um den Parser nicht unnötig kompliziert zu machen, habe ich deshalb auf diese umfangreiche Abfrage verzichtet. Wie umfangreich diese Abfrage ist, werden Sie spätestens dann bemerken, wenn Sie versuchen, sie zu programmieren. Ich rate Ihnen, bei einem Basic-Parser auf diese Abfragerei zu verzichten, denn sie kostet viel zuviel kostbare Rechenzeit, und der Parser wird zu langsam! |
| 50630-50660 | Diese Zeilen treten dann in Kraft, wenn ein UND oder ein Komma gefunden worden ist. Zeiger ZA wird auf eine Stelle nach ZB gesetzt. |
| 50540-50550 | Hier erfolgt das bereits beschriebene Weiterrücken von Zeiger ZB nach rechts, bis ein Leerzeichen oder das Satzende gefunden wird. Wird hier ein Satzende festgestellt, so heißt dies, daß nach dem UND oder Komma nichts mehr kommt. Der Parser äußert sich im Fall zum Beispiel BE$ = »NIMM SCHWERT UND« empört mit »Und was?«, wobei er wieder einen Hauch von Intelligenz beweist und den Spieler in Erstaunen versetzt ... |
| 50670 | Wird eines der Schlüsselwörter IHN, SIE oder ES gefunden, so weiß der Parser sofort, daß der Spieler sich auf das zuletzt erwähnte Objekt bezieht. Das zuletzt erwähnte Objekt wird immer in der Variablen OM gespeichert. Der Parser braucht hier also nichts weiter zu tun, als der Objektzahl 01 den Wert OM zuzuweisen und dann die binäre Suchroutine zu überspringen mit GOTO 51300. |
| 51000-51330 | Hier steht die binäre Suchmethode, mit deren Hilfe SU$ in der relativen Wortschatzdatei gesucht wird. |
| 51008 | Der ASC-Wert des 1. Buchstabens vom Suchwort SU$ wird in der Variablen I gespeichert. |
| 51009 | Wenn nicht der ASC-Wert eines der Buchstaben von A-Z ist, also I-65 kleiner 0 oder I-65 größer 25, dann ist klar, daß das Suchwort nicht mit einem Buchstaben, sondern mit irgendeinem Zeichen beginnt, zum Beispiel SU$= "/%&%/". Es erfolgt dann eine »ICH KENNE DIESES WORT NICHT«-Meldung, und der Codiervorgang wird abgebrochen. |
| 51010 | Der Anfang des zu durchsuchenden Bereiches wird mittels der Index-Tabelle IN(0)-IN(26) ermittelt und in AN gespeichert. |
| 51012-51015 | Hier wird das Ende des Suchbereiches, EN ermittelt. Das Ende berechnet sich NICHT einfach durch EN=IN(I-64), da berücksichtigt werden muß, daß in der Index-Tabelle für nicht existierende Anfangsbuchstaben (zum Beispiel C) eine Null eingetragen ist. Es darf also nicht der unmittelbare nächste Buchstabe zur Ermittlung des Suchendes herangezogen werden, sondern vielmehr der nächste existente Buchstabe (Tabellenwert ungleich 0). |
| 51016 | Wenn AN=0 ist, so beginnt das Suchwort SU$ mit einem Buchstaben, mit dem kein Wort des Wortschatzes beginnt. Es braucht dann gar nicht erst lange gesucht werden – Fehlermeldung ICH KENNE WORT NICHT. |
| 51020-51200 | Hier steht die binäre Suchroutine, die ich Ihnen bereits im Kapitel Dateiverwaltung beschrieben habe. Allerdings ist die Routine diesmal so modifiziert, daß sie mit relativen Dateien arbeitet und nicht mit Stringfeldern im Speicher. |
| 52100-52180 | Dieses Unterprogramm wird innerhalb der binären Suchroutine angesprungen. Es dient dazu, um aus einer relativen Datei einen Satz zu lesen. Diese Routine ist im wesentlichen eine Kombination unserer bisher verwendeten Unterprogramme zum Positionieren auf (GOSUB 9000) und Lesen (GOSUB 9100) eines Satzes aus einer relativen Datei. |
| 52100-52130 | Positionieren und anschließendes Lesen des Satzes A$. |
| 52140 | Die Wortart WA wird ermittelt. |
| 52150 | Der Wortcode WC wird ermittelt. |
| 52160 | AS wird auf das eigentliche Wort gekürzt. Aus A$=»abwaerts 2010« wird A$=»abwaerts «. |
| 52170 | Alle unnötigen Leerstellen in A$ werden gekürzt. Aus A$=»abwaerts « wird also A$=»abwaerts«. Dies ist eigentlich nur dann nötig, wenn es sich bei A$ um ein Objekt handelt (WA=3), denn schließlich werden nur Objekte »rechtsbündig« verglichen beim Suchvorgang. Wer will, kann folgende Zeile einfügen (ohne Gewähr): 52165 IF WA=3 THEN RETURN Warum ohne Gewähr? Während man einen Parser programmiert oder dokumentiert, entdeckt man ständig kleine Dinge, die man noch zu verbessern glaubt. Schnell sind drei, vier Änderungen gemacht, und plötzlich läuft der Parser nicht mehr so recht, obwohl alle Änderungen für sich allein betrachtet doch eigentlich funktionieren sollten!?! Ich rage Ihnen daher, an einem Parser-Programm nie mehr als eine Änderung auf einmal vorzunehmen und dann auch gleich alle Fälle auszuprobieren, um sicherzustellen, daß auch alles noch funktioniert... |
| 51070-51125 | Hier werden die einzelnen Wortzahlen VE,01 etc. ermittelt. Es wird also genau hier codiert. |
| 51080 | Verben werden, wie bereits erklärt, linksbündig gesucht beim Vergleich – also IF SU$=LEFT$(A$, LEN(SU$))... Es sind somit auch Abkürzungen legitim – zum Beispiel NIM statt NIMM oder INV statt INVENTUR. Eine Abkürzung muß allerdings mindestens 3 Zeichen lang sein. Ist die Abkürzung kleiner als 3 Zeichen, so werden Verbsuche und Objektsuche übersprungen und der Parser stellt sich darauf ein, eine Richtungsangabe zu finden. Würde man auf diese Abfrage verzichten, so wüßte der Parser nicht, wie er sich im Fall einer Befehlseingabe BE$=»N« verhalten soll. Er würde als Verbzahl VE das nächstbeste Verb ermitteln, das mit N beginnt, und anschließend auch noch die Richtungsangabe beziehungsweise Richtungszahl RI für die Richtung Norden ermitteln. |
| 51090 | Hier erfolgt die linksbündige Verbsuche. |
| 51100 | Hier erfolgt die Objektsuche – und zwar wie bereits erklärt rechtsbündig, damit zum Beispiel sowohl HOLZKISTE als auch KISTE verstanden werden, wenn der Objektname HOLZKISTE lautet. |
| 51102 | Jetzt wird's kompliziert zu erklären! Es wird überprüft, ob bei der letzten Codierung wegen Auffinden eines UNDs beziehungsweise Kommas abgebrochen wurde (UD größer 0). Ist dies der Fall, so werden 01 und 02 gelöscht und UD auf 3 gesetzt, damit beim Auffinden eines zweiten Objekts nicht wieder gelöscht wird. |
| 51104-51105 | In einem Befehlssatz können maximal zwei Objekte gleichzeitig auftreten (beziehungsweise Teilbefehl) – vergleiche Wortschatzlehre! Es müssen also zwei Variablen zum Speichern von Objekten zur Verfügung stehen – 01 und 02. Nehmen wir einmal BE$=»ÖFFNE TÜR MIT DEM SCHLÜSSEL UND BETRETE DAS ZIMMER« an. Zuerst wird ÖFFNE im Wortschatz gesucht und eine Verbzahl VE gefunden. Dann wird das Wort TÜR gesucht und gefunden. Das Objekt TÜR wird codiert in der 1. Objektzahl 01 gespeichert. Das Wort MIT wird nicht berücksichtigt (ich setze hier einmal voraus, daß es im Wortschatz als AUS-Wort aufgeführt ist). Jetzt wird das Objekt SCHLÜSSEL gesucht und gefunden. In der Variablen 01 kann sein Code nicht gespeichert werden, da 01 bereits belegt ist. In 02 ist allerdings noch Platz. Dem Parser ist also klar, daß ein zweites Objekt gefunden wurde und dies in 02 gespeichert werden muß. Nehmen wir jetzt einmal den unmöglichen Fall an, daß in einem Befehlssatz oder Teilbefehl 3 Objekte auftauchen. Das dritte Objekt wird vom Parser dann eiskalt ignoriert, und es erfolgt keine Fehlermeldung. Wenn in einem Befehlssatz 3 Objekte verwendet werden, so ist klar, daß der Befehlssatz nur unsinnig sein kann, zum Beispiel »ERSCHLAGE MONSTER MIT DEM SCHWERT DAS AUF DEM STUHL LIEGT«. Ok, eigentlich ergibt dieser Satz einen Sinn, aber wir wissen, daß dies nicht die Eingabe-Syntax eines routinierten Adventure-Spielers ist. Man dürfte es dem Parser also nicht übel nehmen, wenn er sich mit »ICH VERSTEHE SIE NICHT« meldet. Man könnte also (wieder ohne Gewähr) noch folgende Zeile einbauen: 51103 IF O1<>0 AND O2<>0 THEN? »ICH VERSTEHE SIE NICHT«:GOTO50000. Als nächstes Wort wird UND gefunden. Die Variable UD wird auf 1 gesetzt und der Codiervorgang abgebrochen. Der erste Teilbefehl des gesamten Befehlssatzes ist somit codiert und liegt anhand der Variablen VE,01,02 und sekundär UD vor. Nach Analyse dieses Teilbefehls wird die Codierung fortgesetzt. Zunächst wird BETRETE als neues VERB ermittelt. Aus der Wortschatzlehre wissen Sie bereits, daß ein Befehlssatz maximal ein Verb enthalten kann. Stehen unsinnigerweise doch mal zwei Verben in einem Befehl, so speichert Parser 4.0 den Wert des Verbs in VE ab, das am weitesten rechts im Befehl steht. Jetzt wird nach Ignorieren von DAS das neue Objekt ZIMMER gesucht. Wäre Zeile 51102 nicht vorhanden, so würde der Parser dieses Objekt nicht mehr codieren können, da erst die beiden Werte 01 und 02 gelöscht werden müssen. Jetzt liegt der zweite Teilbefehl von BE$ codiert vor. Und zwar diesmal in den Variablen VE und 01. |
| 51110 | Hier wird eine Richtungsangabe absolut gesucht. |
| 51115 | Adjektivsuche – ebenfalls absolut. Zur Adjektivcodierung wurde nur die Variable AD reserviert. Ein Befehl kann also nur maximal ein Adjektiv enthalten. Dies langt in der Regel. Wer jedoch Adventures programmieren will, bei denen extrem komplizierte Befehlseingaben als BE$=»ÖFFNE DIE GRÜNE TÜR MIT DEM GELBEN SCHLÜSSEL « erforderlich sind, der muß hier entsprechend modifizieren (analog zur Objektcodierung mittels 01 und 02). |
| 51120 | Absolute Verbsuche. Hier werden Verben gefunden, deren Satzlänge weniger als 3 Zeichen beträgt. Wollen Sie zum Beispiel I als Abkürzung für INVENTUR erlauben, so können Sie I extra in der Wortschatzdatei als Verb eintragen. |
| 51125 | AUS-Wörter wie der, die, das werden absolut gesucht und gegebenenfalls ignoriert. |
| 51300-51350 | Weiterrücken der Zeiger ZA und ZB aufs nächste Wort. Eine neue Suchrunde wird eingeläutet. |
| 51500-51555 | Kleine Bierbraukunde |
| 52000-52090 | Wird hier endlich geklärt, was Lolo, Peter und Mike im Wald machen? |
Variablenliste
| IN(0) bis IN(26) Index-Tabelle | |
| UD | UND-Variable (größer 0 bei Codierungsabbruch wegen UND oder ,) |
| SL | Maximal erlaubte Befehlssatzlänge (max. 255 Zeichen!) |
| BE$ | Gesamter Befehlssatz |
| X$ | Hilfsstring |
| I | Hilfsvariable |
| SN | Satzzeichenwert |
| I$ | Hilfsstring |
| VE | Verbzahl |
| RI | Richtungszahl |
| O1 | 1. Objektzahl |
| O2 | 2. Objektzahl |
| ZA,ZB | Zeiger zum Eingrenzen des Suchwortes SU$ in BE$ |
| SU$ | Suchwort |
| IZ | Hilfsvariable |
| AN | Anfang des Suchbereichs |
| EN | Ende " " |
| SZ | Anzahl der Worte in der Wortschatzdatei |
| N | Hilfsvariable |
| SA | Satz einer relativen Datei (Nummer) |
| WA | Wortart |
| WC | Wortcode |
| HB% | Higher Byte |
| LB% | Lower Byte |
| A | Fehlerkanal-Zahl |
| A$ | Satz, der von relativer Datei gelesen wurde. |
Das war's auch schon. Anhand dieser umfangreichen Dokumentation sollte es Ihnen nun nicht mehr schwerfallen, sich mit Parser 4.0 zurechtzufinden und eigene Änderungen am Programm durchzuführen. Mit Sicherheit haben Sie bald so manche Idee, was man noch verbessern kann. Ich rate Ihnen auf jeden Fall, an dieser Stelle erst dann mit dem folgenden Kursabschnitt fortzufahren, wennn Sie Parser 4.0 verstanden haben.
Für Profis: Parser 4.1
Parser 4.1 ist eine Verbesserung beziehungsweise eine schnellere Version von Parser 4.0. Das nun Folgende soll Ihnen lediglich als theoretisches Gedankengut dienen, wenn Sie Parser 4.0 selbständig verbessern wollen.
Parser 4.0 basiert auf dem index-binären Prinzip. Der Wortschatz muß daher alphabetisch geordnet sein, damit eine Index-Tabelle IN(0)-IN(26) erstellt werden kann. Aus dieser Tabelle kann man schnell erlesen, in welchem Satzbereich der relativen Datei Wörter mit gleichem Anfangsbuchstaben stehen. Durch diese Eingrenzung des Suchbereiches werden einzelne Suchwörter mit relativ wenigen Diskzugriffen gefunden. Die Idee von Parser 4.1 ist wie folgt: Auch hier findet die index-binäre Methode Verwendung. Allerdings wird diesmal nicht eine Indextabelle IN(0)-IN(26) verwendet, sondern vielmehr ein Index-Feld, das so angelegt wird:
DIM IN%(26,26)
In IN%(0,0)-IN%(26,0) steht hierbei wieder die gleiche Index-Tabelle, die bei Parser 4.0 in IN(0)-IN(26) steht.
Jetzt spielt allerdings auch noch der 2. Buchstabe des Suchwortes eine Rolle bei der Einschränkung des Suchbereiches.
In IN%(0,0)-IN%(0,26) zum Beispiel ist die Index-Tabelle des zweiten Buchstabens aller Wörter, die mit A beginnen, gespeichert.
Das heißt, in IN%(0,0) steht, wo alle Wörter der Wortschatzdatei beginnen, deren erste beide Buchstaben AA sind. In IN%(0,1) entsprechend AB, in IN%(1,3) entsprechend BD etc. Anhand eines Index-Feldes kann der Suchbereich folglich noch viel feiner eingeschränkt werden als bei einer Index-Tabelle. Das heißt, es werden weniger zeitraubende Disketten-Zugriffe zum Finden eines Suchwortes benötigt, und der gesamte Parser wird schneller.
Parser 4.0 braucht ziemlich lange, um festzustellen, daß er ein mit den beiden Buchstaben RT beginnendes Wort nicht kennt. Parser 4.1 hingegen benötigt nicht einmal einen einzigen Disketten-Zugriff. Er kann aus seinem Index-Feld auf Anhieb erlesen, daß die Buchstabenkombination RT im Wortschatz nicht vertreten ist. Parser 4.0 läßt sich schnell und relativ einfach zu Parser 4.1 aufrüsten. Zunächst müssen Sie den Index-Maker modifizieren, damit er Index-Felder erstellen kann. Der Rest dürfte für Sie dann ein Kinderspiel sein – wenn Sie Parser 4.0 verstanden haben.
Parser 5.0 – der Superparser
Parser 4.0 ist ja recht nett. Man kann relativ umkompliziert den Wortschatz erweitern und schnell Programmänderungen vornehmen. Trotzdem reicht Parser 4.0 in puncto Geschwindigkeit keineswegs an den INFOCOM-Parser heran. Parser 4.0 braucht im Durchschnitt 15 Sekunden zum Codieren eines Befehls, Parser 4.1 etwa die Hälfte. Mit dem Codieren ist es allerdings noch lange nicht abgetan – bis auf dem Bildschirm eine Antwort erscheint, muß die Codierung erst noch analysiert werden etc. Um eine INFOCOM ähnliche Geschwindigkeit zu erreichen, muß man daher einen möglichst schnellen Parser haben, um Zeit für die Analysenberechnung zu sparen. Wie macht's INFOCOM? Mit Sicherheit verwaltet INFOCOM seine 1000 Wörter NICHT in Form einer relativen Datei. Zwar beginnt nach jeder Befehlseingabe die Floppy zu laufen, jedoch nur, um den Antwortsatz einzulesen. Codiert ist er ruck, zuck. Um solche Geschwindigkeiten zu erreichen, hilf die schnellste Floppy beziehungsweise relative Datei-Programmierung nicht weiter. Der Wortschatz muß irgendwo im RAM untergebracht werden. Nehmen wir einmal an, wir möchten im Speicher 600 Wörter unterbringen. Für jedes Wort haben wir uns 21 Zeichen +4 Zeichen für Wortart und Wortcode reserviert. Wir bräuchten also 600 mal 25 Byte RAM, um diesen Wortschatz im RAM zu verstauen. Also einen freien RAM-Bereich von zirka 15 KByte. Abgesehen vom Basic-RAM verfügt der C 64 über freies RAM von 40960-65535 – also 25 KByte. Für den, der englische Textadventures programmieren will, ist das Problem damit auch schon gelöst. Wir beanspruchen für unsere Adventures jedoch RAM zur Unterbringung des deutschen Zeichensatzes. Trotzdem bleiben uns noch 12 KByte RAM am Stück – nämlich von 40960 bis 52223. Das ist immerhin genug Platz für zirka 480 Wörter – vorausgesetzt, wir halten an unserem bisherigen Wortschatzaufbau fest. Wenn Sie sich unseren Wortschatz einmal anschauen, so werden Sie schnell feststellen, daß eigentlich kein Verb länger als 16 Zeichen ist. Zum Speichern von Wortcode und Wortart genügen eigentlich auch 2 Byte (vorausgesetzt, es gibt maximal 255 verschiedene Wörter einer Wortart wie zum Beispiel Verben). So würden uns schon 18 Byte zum Speichern eines Wortes genügen. Wir würden dann 12000/18 = 682 Wörter im RAM unterbringen, womit unser erstes Ziel erreicht wäre.
Der RAM-Editor
Bevor man allerdings einen Wortschatz innerhalb eines freien RAM-Bereiches verwalten will, muß man erst einmal einen Wortschatz ins RAM bringen. Wie man das am besten macht? Nun, die Worte einzeln per Hand ins RAM POKEn ist mit Sicherheit nicht das Gelbe vom Ei. Eine Idee wäre es vielleicht, einen Editor zu schreiben, der für diese Arbeit geeignet ist. Zusätzlich würde man dann natürlich noch einen Supersorter benötigen, der die Worte in diesem RAM-Bereich sortieren kann. Diese beiden Hilfsprogramme zu programmieren macht sehr viel Arbeit. Für uns gibt es hier eine Alternative, die außerdem sogar noch ihre Vorteile hat: Schließlich haben wir doch schon unseren relativen Datei-Editor und einen Supersorter, mit denen wir relative Dateien beziehungsweise relative Wortschätze prima in den Griff bekommen. Wir brauchen also nur ein Programm, das es uns ermöglicht, relative Dateien von der Diskette ins RAM zu schreiben. Wie diese RAM-Dateien dann verwaltet werden, soll uns an dieser Stelle noch nicht interessieren. Betrachten wir doch noch einmal einen kleinen Teil unserer Wortschatz-Datei:
1 abwaerts 2010
2 aufwaerts 2009
3 bedanken 1074
4 bedecken 1037
5 befestigen 1042
Dies sind die ersten fünf Sätze der relativen Wortschatz-Datei. Nehmen wir nun einmal an, diese fünf Sätze sollen ins RAM ab Speicherzelle 40960 geschrieben werden. Wir wissen, daß sich jeder Satz aus 25 Zeichen zusammensetzt:
| 1-2 | das eigentliche Wort |
| 22 | die Wortzahl |
| 23-25 | der Wortcode |
Nehmen wir außerdem an, daß wir zur Speicherung von Wortzahl und Wortcode jeweils nur 1 Byte benötigen (also maximal 255 Verben, 255 Objekte etc.), so können wir davon ausgehen, daß wir zur Speicherung eines Satzes der relativen Datei 21+2=23 Byte RAM benötigen. Für fünf Sätze sind dies 5 mal 23 = 115 Byte. Wir beanspruchen also den RAM-Speicher von 40960-41075. Bild 10 zeigt, wie die Sätze im RAM stehen.

Satz 1 steht jetzt also ab Speicherzelle 40960, Satz 2 ab 40983 und so weiter. In Adresse 40960+21 steht die Wortzahl, in 40960+22 der Wortcode des 1. Wortes. Entsprechendes gilt für alle anderen Sätze. Sie sehen schon, daß es gar nicht so schwer ist, im RAM ein relative-Dateien-ähnliches Verwaltungssystem aufzubauen. Bevor wir uns jedoch näher mit der Verwaltung von RAM-Dateien beschäftigen, brauchen wir erst einmal ein Programm (Listing 9), mit dem es möglich ist, relative Dateien kurz und schmerzlos in RAM-Dateien umzuwandeln.
Der RAM-Editor ist ein reines Hilfsprogramm. Ich verzichte daher auf eine Dokumentation – es gibt auch kaum eine sinnvolle Idee, um die man den Editor noch erweitern könnte.
Lassen Sie uns nun unsere relative Wortschatzdatei in eine RAM-Datei umwandeln: Nach dem Programmstart fordert der RAM-Editor Sie auf, den Namen der Datei anzugeben, von der Daten ins RAM geschrieben werden sollen. Geben Sie hier »Wortschatz« an, legen Sie die Diskette, auf der sich die Wortschatz-Datei befindet, ins Laufwerk und drücken Sie RETURN. Sodann müssen Anfang und Ende des Bereiches angegeben werden, der ins RAM übertragen werden soll. Wir wollen die gesamte Wortschatzdatei umwandeln: also Anfang = 1 und Ende = 197 (da wir 197 Wörter im Wortschatz haben). Nun müssen Anfang und Ende des RAM-Bereiches angegeben werden, in dem die Daten später stehen sollen. Um das Programm allgemein zu halten, müssen Sie dies selbst ausrechnen. Wir wollen unseren Wortschatz ab Speicherstelle 40960 unterbringen. Warum der Editor nicht selbständig ermitteln kann, erkläre ich Ihnen gleich. Wir haben 197 Wörter und brauchen pro Wort 23 Byte RAM (wie wir bereits ermittelt haben). Es werden für den gesamten Wortschatz also 197 mal 23 = 4531 Byte benötigt. Das Ende des RAM-Bereiches errechnet sich also aus 40960 + 4531 = 45491. Jetzt wird der erste Satz der relativen Datei eingeblendet. Anhand der Cursortasten left-right und der Zifferntasten 1 bis 0 können Sie jetzt einzelne Satzteile markieren. In Bild 11 können Sie sehen, wie im Falle unserer Wortschatz-Datei markiert werden muß. Der Markierungsvorgang wird mit der RETURN-Taste abgeschlossen. Jetzt erscheint auf dem Bildschirm, wie der eben markierte Satz in einzelne Bereiche zerlegt wurde. Es können maximal 3 Bereiche markiert werden – in beliebiger Reihenfolge! Sie müssen nun zu jedem einzelnen Bereich angeben, ob er als VAL-Wert oder als String gespeichert werden soll. Bei String wird der Bereich einfach in ASC-Zahlen umgewandelt, die ins RAM gePOKEt werden. Bei VAL wird aus dem Bereich ein VAL-Wert ermittelt (der nicht größer als 255 sein darf – gegebenenfalls müssen Sie in High- und Low-Byte aufteilen), der dann einfach an die entsprechende Stelle im RAM gePOKEt wird. Sie müssen nun eine Zeitlang warten. Dann werden Sie aufgefordert, einen Filenamen anzugeben, unter dem die Wortschatz-RAM-Datei als PRG-File auf Diskette gespeichert wird. Bitte legen Sie hier eine leere Diskette ein und geben Sie als Filenamen »Wortschatz« an. Jetzt dauert's wieder eine Weile. Nun kann ein Index-File erstellt werden, was bei Wortschatz-Dateien in der Regel notwendig ist. Es müssen wieder Anfang und Ende des RAM-Bereiches angegeben werden und außerdem die Schrittweite – dies ist die Länge jedes Satzes im RAM – in unserem Fall 23 (Byte). Jetzt speichern Sie den erstellten Index unter dem Filenamen »Index« ab, und fertig ist die RAM-Wortschatzdatei. Dies alles hört sich viel komplizierter an, als es eigentlich ist. Bitte vergleichen Sie stets mit Bild 11, damit Sie beim Erstellen der RAM-Datei nichts falsch machen!

Die Wortschatz-Datei kann jetzt mittels LOAD»WORTSCHATZ«,8,1 geladen werden.
Der Index ist als SEQ-File gespeichert und kann folgendermaßen eingelesen werden:
5 OPEN3,4
10 OPEN1,8,2,"INDEX"
20 FORI=1TO26
30 :INPUT#1,X
40 :PRINT#3,CHR$(64+I)" AB ADRESSE"X
50 NEXTI
60 CLOSE1
70 CLOSE3
Wenn Sie alles richtig gemacht haben, dann müssen Sie jetzt folgende Wertetabelle (bei Ausgabe auf Drucker) erhalten:
A AB ADRESSE 40960
B AB ADRESSE 41006
C AB ADRESSE 0
D AB ADRESSE 41581
E AB ADRESSE 41650
F AB ADRESSE 42018
G AB ADRESSE 42110
H AB ADRESSE 42317
I AB ADRESSE 42501
J AB ADRESSE 0
K AB ADRESSE 42547
L AB ADRESSE 42685
M AB ADRESSE 0
N AB ADRESSE 42823
O AB ADRESSE 43007
P AB ADRESSE 43076
Q AB ADRESSE 43237
R AB ADRESSE 43260
S AB ADRESSE 43605
T AB ADRESSE 44364
U AB ADRESSE 44502
V AB ADRESSE 44571
W AB ADRESSE 45031
X AB ADRESSE 0
Y AB ADRESSE 0
Z AB ADRESSE 45284
Jetzt steht der Programmierung von Parser 5.0, dem Superparser, nichts mehr im Wege. Im Prinzip ist es sogar ein leichtes, unseren Parser 4.0 so zu modifizieren, daß er statt mit einer relativen Datei mit einer RAM-Datei arbeitet. Dies ist jedoch nicht empfehlenswert, da es bei RAM-Dateien wesentlich effektivere Verwaltungsmöglichkeiten gibt, die allerdings etwas komplizierter als bei der relativen Disk-Verwaltung verlaufen.
Leider nicht komplett
Um Ihnen eine Enttäuschung zu bewahren, muß ich Sie bereits an dieser Stelle darauf aufmerksam machen, daß Sie auf diesen Seiten keineswegs einen kompletten Parser 5.0 finden. Es gibt einfach zu viele Möglichkeiten bei der Verwaltung von RAM-Wortschätzen, von denen Sie am besten selbst entscheiden, welche Ihnen am meisten behagt, nachdem ich Ihnen verschiedene vorgestellt habe.
Bitte geben Sie jedoch zunächst einmal Listing 10 ein.
Dieses Programm ist, wie bereits gesagt, noch kein kompletter Parser. Es soll Ihnen vielmehr als Grundgerüst eines Parsers 5.0 nach eigenen Vorstellungen dienen.
Dokumentation zum Listing:
| 10 | Laden des Wortschatzes von Diskette ins RAM. |
| 30 | Indextabelle IN (1)-IN(26) initialisieren |
| 40-80 | Index-DATAs. Hier wurde nichts weiter gemacht, als der Index-File ausgelesen und in DATAs untergebracht. |
| 100-160 | Hier steckt eine für Parser 5.0 grundlegende Erweiterung: Eine modifizierte PEEK-Funktion, die es erlaubt, die Speicherzellen unter dem Basic-ROM auszulesen. Sie wissen mit Sicherheit bereits, daß beim POKEn in diesen Bereich immer ins RAM, aber beim normalen PEEKen immer der Speicherinhalt vom ROM ausgegeben wird. Die erweiterte PEEK-Funktion wird mittels USR(X) aufgerufen. Wenn Sie zum Beispiel in Speicherzelle 60000 den Wert 1 schreiben mit POKE 60000,1, so erhalten Sie bei PRINT PEEK (60000) einen Wert, der nicht unbedingt der 1 entspricht. PRINT USR(60000) zeigt jedoch den Wert vom RAM: 1. Diese Erweiterung ist für alle diejenigen unentbehrlich, die Parser 5.0 ausschließlich in Basic programmieren wollen. |
| 200 | Aufruf von Parser 5.0 |
| 50010-50130 | Erweiterte INPUT-Routine. Diesmal mit schönem Eingabe-Ton. |
| 50200 | Das Herzstück von Parser 5.0 beginnt hier. |
| -50290 | Er erfolgt hier die Suche nach dem Wort. Das Programm ist sehr einfach gehalten. Es können nur Befehlssätze eingegeben beziehungsweise codiert werden, die nicht mehr als ein Wort enthalten. Sie können dies anhand des bisher erworbenen Wissens jedoch leicht ändern. |
| 50254 | Das Suchwort I$ soll im RAM gesucht werden. |
| 50255 | In der Variablen IM wird der ASC-Wert des ersten Buchstabens vom Suchwort gespeichert. Anhand der Index-Tabelle wird jetzt ermittelt, ab welcher Speicherzelle (=I) die Wörter stehen, deren erster Buchstabe den ASC-Wert IM hat. |
| 50262-50280 | Hier steht die eigentliche Suchroutine. |
| 50264 | Wenn IM nicht gleich USR(I) ist, so heißt dies, daß an der RAM-Stelle I ein Wort steht, dessen erster Buchstabe nicht mit dem Suchwort I$ übereinstimmt. Der Suchvorgang wird dann mittels GOTO 50290 abgebrochen, und es erfolgt die altbekannte Fehlermeldung »ICH KENNE DIESES WORT NICHT!«. |
| 50265 | Der gesamte Suchbegriff I$ wird vom ersten bis zum letzten Zeichen durchlaufen. Es wird dabei verglichen, ob ASC(MID$(I$,1,1)-ASC(MID$(I$,LEN(I$), 1) gleich USR(I-1) - USR(I-1+LEN(I$)) ist. Es werden also der Reihe nach die einzelnen Buchstaben des Suchwortes mit den entsprechenden Speicherzellen im RAM verglichen, von denen vermutet wird, daß sie dem Buchstaben entsprechen. |
| 50270 | Wird bei irgendeinem Buchstaben des Wortes eine Nichtübereinstimmung mit dem Wert im RAM festgestellt, so wird der Suchvorgang abgebrochen mit GOTO 50276, und der Zeiger I wird auf das nächste Wort im RAM gesetzt, dessen erster Buchstabe IM ist: I=I+23 (23, da jedes Wort beziehungsweise jeder Satz 23 Byte lang ist). |
| 50273-50275 | Wurde bis zum letzten Buchstabenvergleich keine Nichtübereinstimmung festgestellt, so wurde das Suchwort im RAM gefunden. Anhand der Wortart WA=USR(I+21) wird dann die entsprechende Wortzahl WA=USR(I+22) ermittelt. Das Wort ist somit codiert. |
Wenn Sie das Programm einmal laufen lassen, werden Sie überrascht sein, wie schnell die Wörter im RAM gefunden werden. Schon das Grundmodell von Parser 5.0 ist weitaus schneller als Parser 4.0 und Parser 4.1. Allerdings läßt sich die Verarbeitungsgeschwindigkeit noch erhöhen:
Man kann zum Beispiel eine index-binäre Suchroutine wie bei Parser 4.0 einbauen. Ergänzt man diese index-binäre Methode zu einer index-Feld-binären Suchroutine, so erhält man einen Basic-Parser, wie man ihn sich schneller kaum noch vorstellen kann.
Ein index-Feld-binärer Parser 5.0 kann an Geschwindigkeit nur noch mit Maschinensprache-Programmierung übertroffen werden. Allerdings kann man in puncto Wortschatz-Organisation noch einiges verbessern, wie die folgenden Beispiele zeigen:
Verbesserte Wortschatz-Organisation
Wie wir bereits festgestellt haben, besteht ein Wortschatz aus Verben, Objekten, Adjektiven und Richtungsangaben und AUS-Wörtern. Als Wortlänge haben wir uns bisher maximal 21 Zeichen reserviert. Sind aber wirklich 21 Zeichen nötig? – Mit Sicherheit nicht. Ein Verb zum Beispiel ist in 99% aller Fälle an 6 Buchstaben zu erkennen. Das heißt, kennt man die ersten 6 Buchstaben eines Verbs, so weiß man mit großer Wahrscheinlichkeit, welches Verb gemeint ist.
Betrachten wir doch einmal die ersten 6 Sätze unserer Wortschatz-Datei im Falle einer Verbkürzung auf 6 Zeichen:
abwaer 2010
aufwae 2009
bedank 1074
bedeck 1073
befest 1042
beklei 1065
belohn 1046
benutz 1063
beraub 1058
berueh 1017
Bei einer Umwandlung zur RAM-Datei benötigt man in diesem Fall nur noch 8 Byte – das heißt, man bringt fast dreimal soviele Verben unter wie bisher!
Es ist auch ein leichtes, eine neue Wortschatzdatei zu erstellen, bei der die Verben auf 6 Zeichen Länge begrenzt werden. Sie müssen beim Markieren mit dem RAM-Editor lediglich nur die ersten 6 Zeichen markieren. Bei der Suchroutine ist jetzt allerdings zu beachten, daß im Falle einer Verb-Suche nur noch die ersten 6 Buchstaben des Suchwortes im RAM gesucht werden. Und zwar linksbündig, wie es sich für die Verbsuche gehört. Analog für die Verben kann man auch die Adjektive und Richtungsangaben abhandeln. Bei den AUS-Wörtern, also den Wörtern, die eigentlich zur Codierung gar nicht benötigt werden (zum Beispiel der, die, das, den etc.), gilt dies im großen und ganzen auch.
Etwas schwieriger ist es mit den Objekten. Objekte können sehr lange Namen haben, wie zum Beispiel »Fernsehapparat« oder »Zeitmaschine«.
Behalten wir als Musterbeispiel einmal den Objektnamen »Zeitmaschine« bei. Objekte haben wir bei Parser 4.0 rechtsbündig gesucht (damit sowohl TÜR als auch HAUSTÜR verstanden wird etc.). Die letzten 6 Buchstaben von »Zeitmaschine« lauten »schine«, und daraus läßt sich wahrlich nicht erkennen, daß es sich um eine »Zeitmaschine« handelt. Würde man für die Objekte 10 Buchstaben reservieren, so käme man schon besser zurecht. Bei relativen Dateien muß jeder Satz die gleiche Länge haben, und es kann immer nur eine relative Datei gleichzeitig geöffnet sein. Deshalb waren wir bei Parser 4.0 und 4.1 auch gezwungen, eine große Wortlänge für die Verben zu reservieren, obwohl diese eigentlich nur für Objekte nötig ist. Eine Alternative wäre es gewesen, Objekte und die anderen Wortarten zusammen, in zwei getrennten relativen Dateien unterzubringen. Das Öffnen und Schließen von relativen Dateien wäre dann jedoch unvermeidbar gewesen. Das hätte Parser 4.0 stark verlangsamt, weil das Öffnen von relativen Dateien viel Zeit in Anspruch nimmt. Also eine miserable oder keine Alternative. Anders bei RAM-Dateien. Von ihnen können beliebig viele gleichzeitig geöffnet werden beziehungsweise sind immer offen. Es bietet sich also an, für die Objekte einen anders organisierten RAM-Bereich herzunehmen. Dies ist auch der Grund, warum ich unseren bisherigen Wortschatz frei von Objekten, nur aus Verben bestehend, gelassen habe. Ich bin mir ziemlich sicher, daß Sie bei Ihren eigenen Adventures zu den vorhandenen 197 Verben und Richtungsangaben kaum noch mehr als 50 andere finden. Wenn Sie sich also Platz für 400 Wörter mit 6 Zeichen Länge (250 Verben + 150 Adjektive) RAM reservieren wollen, so brauchen Sie 400 mal 8 = 3200 Byte Speicherplatz. Für Verben, Richtungsangaben und Adjektive und eventuell auch noch AUS-Wörter können wir das freie RAM von 53248 bis 57343 (= Platz für 511 Wörter mit 8 Zeichen Länge – mit 2 Zeichen gerechnet für Wortart und Wortcode). Jetzt haben wir noch freies RAM von 40960 bis 52222 – also genug Platz für 938 Objekte bei Objektlänge = 11 Zeichen + Wortcode (auf die Wortart können wir verzichten, da wir wissen, daß sich in diesem Bereich nur Objekte befinden).
938 + 511, das bringt einen Wortschatz von 1449 Wörtern. Was soll also das unsinnige Infocom-Getue um den fantastischen 1000er-Wortschatz. 1449 Wörter, das sind mehr Wörter, als der englische Grundwortschatz hat, mit dem sich immerhin schon 70 % aller Dinge in Englisch formulieren lassen, sind garantiert genug für jedes nur erdenkliche Adventure, das auf einem C 64 geschrieben werden soll. Jetzt liegt es nur noch an Ihnen, Parser 5.0 zu programmieren. Wenn Sie das gesamte Parser-Technik-Kapitel aufmerksam durchgearbeitet haben, dürfte Ihnen diese Programmierung keine Schwierigkeiten mehr bereiten (wobei Sie noch nicht einmal in Maschinensprache programmieren müssen)!
Bild 12 zeigt, wie es jetzt im RAM des C 64 aussieht:

Der freie RAM-Bereich ist nahezu voll ausgenutzt mit dem Wortschatz und dem deutschen Zeichensatz. Besser kann man das RAM im Bereich der Adventureprogrammierung garantiert nicht mehr ausnutzen. Sogar die Grafikroutine (vergleiche Kapitel Grafikprogrammierung) läßt sich in dieses RAM-Schema noch integrieren – es geht dann nur Basic-RAM verloren sowie 1 KByte des Objekt-RAMs, wobei aber immer noch Platz für genügend Objekte vorhanden ist. Den RAM-Bereich von 61440 bis 65535 lasse ich Ihnen für eigene Ideen offen. Den Bereich von 1024 bis 2048, der durch das Verschieben des Bildschirms freigeworden ist, werde ich allerdings noch beanspruchen.
Wir haben es also geschafft – wir haben viele Basic-Routinen und Parser kennengelernt und das RAM des C 64 optimal mit Wortschatz, Grafikfenster und deutschem Zeichensatz belegt. Es steht also nur noch aus, das ganze Wissen zusammenzupacken und daraus Adventures zu programmieren. Wie das geht, erfahren Sie im anschließenden Kapitel.
(M. Nickles/bs)0 rem *********************************
1 rem * index-maker - erstellt den *
2 rem * index zu einem beliebigen, *
3 rem * alphabetisch geordneten *
4 rem * bereich einer relativen datei *
5 rem * <c> 1986 bei michael nickles *
6 rem *********************************
10 print"{clr}{yel}{rvon}index-maker{rvof}"
20 dim m%(26) : rem index-tabelle
30 print"{down}bitte geben sie den namen der relativen
40 print"datei an, von der ein index erstellt
50 print"werden soll!"
60 poke198,0:input"{down}";d$
70 open1,8,3,d$:open15,8,15:rem datei oeffnen
80 print"{down}bitte geben sie den bereich der datei an";
90 print"von dem der index erstellt werden soll!"
100 poke198,0:input"{down}anfang";an
110 poke198,0:input"{down}ende ";en
120 sa=an:gosub9000:ifa=50thenprint"anfangsatz existiert nicht!":goto100
130 sa=en:gosub9000:ifa=50thenprint"endsatz existiert nicht!":goto100
140 ifan>enthenprint"unsinn!":goto100
1000 rem index-erstellen -------------------------------------------------------
1001 :
1005 print"{down}bitte warten !"
1010 sa=an:gosub9000
1020 iz=0
1030 forsa=antoen
1035 :
1040 : gosub9100:printsa,a$
1050 : ifm%(asc(left$(a$,1))-64)=0thenm%(asc(left$(a$,1))-64)=sa
1080 :
1100 nextsa
2000 rem index ausgeben --------------------------------------------------------
2001 :
2010 print"{clr}index-ausgabe:"
2020 print"{down}{down}ausgabe auf bildschirm <1>
2030 print"{down}als seq-file auf disk <2>
2040 poke198,0
2050 geti$:ifi$<>"1"andi$<>"2"then2050
2060 ifi$="1"then2100
2070 ifi$="2"then2200
2100 rem ausgabe auf bildschirm
2101 :
2110 print"{clr}"
2120 fori=1to13:printchr$(i+64)"="m%(i),chr$(i+13+64)"="m%(i+13):nexti
2130 end
2200 rem m%(1)-m%(26) als seqfile speichern
2201 :
2210 poke198,0:input"{down}filename ";i$:i$=i$+",s,w"
2220 open2,8,2,i$
2230 fori=1to26
2240 :print#2,m%(i)
2250 nexti:close2
2260 end
9000 rem zeiger positionieren --------------------------------------------------
9001 :
9010 hb%=sa/256:lb%=sa-hb%*256
9020 print#15,"p";chr$(3);chr$(lb%);chr$(hb%);chr$(1)
9030 input#15,a:ifa=50thenprint"satznummer zu hoch"
9040 return
9100 rem satz lesen ------------------------------------------------------------
9101 :
9110 ifa=50thenreturn
9120 input#1,a$
9130 return
0 REM ********************************* 1 REM * INDEX-MAKER - ERSTELLT DEN * 2 REM * INDEX ZU EINEM BELIEBIGEN, * 3 REM * ALPHABETISCH GEORDNETEN * 4 REM * BEREICH EINER RELATIVEN DATEI * 5 REM * <C> 1986 BEI MICHAEL NICKLES * 6 REM ********************************* 10 PRINT"{CLR,YELLOW,RVSON}INDEX-MAKER{RVOFF}" 20 DIM M%(26) : REM INDEX-TABELLE 30 PRINT"{DOWN}BITTE GEBEN SIE DEN NAMEN DER RELATIVEN 40 PRINT"DATEI AN, VON DER EIN INDEX ERSTELLT 50 PRINT"WERDEN SOLL!" 60 POKE 198,0:INPUT"{DOWN}";D$ 70 OPEN 1,8,3,D$:OPEN 15,8,15:REM DATEI OEFFNEN 80 PRINT"{DOWN}BITTE GEBEN SIE DEN BEREICH DER DATEI AN"; 90 PRINT"VON DEM DER INDEX ERSTELLT WERDEN SOLL!" 100 POKE 198,0:INPUT"{DOWN}ANFANG";AN 110 POKE 198,0:INPUT"{DOWN}ENDE{SPACE,SPACE}";EN 120 SA=AN:GOSUB 9000:IF A=50 THENPRINT"ANFANGSATZ EXISTIERT NICHT!":GOTO 100 130 SA=EN:GOSUB 9000:IF A=50 THENPRINT"ENDSATZ EXISTIERT NICHT!":GOTO 100 140 IF AN>EN THENPRINT"UNSINN!":GOTO 100 1000 REM INDEX-ERSTELLEN ------------------------------------------------------- 1001 : 1005 PRINT"{DOWN}BITTE WARTEN !" 1010 SA=AN:GOSUB 9000 1020 IZ=0 1030 FOR SA=AN TOEN 1035 : 1040 : GOSUB 9100:PRINTSA,A$ 1050 : IF M%(ASC (LEFT$ (A$,1))-64)=0 THENM%(ASC (LEFT$ (A$,1))-64)=SA 1080 : 1100 NEXT SA 2000 REM INDEX AUSGEBEN -------------------------------------------------------- 2001 : 2010 PRINT"{CLR}INDEX-AUSGABE:" 2020 PRINT"{2DOWN}AUSGABE AUF BILDSCHIRM <1> 2030 PRINT"{DOWN}ALS SEQ-FILE AUF DISK{SPACE,SPACE}<2> 2040 POKE 198,0 2050 GET I$:IF I$<>"1"AND I$<>"2" THEN2050 2060 IF I$="1" THEN2100 2070 IF I$="2" THEN2200 2100 REM AUSGABE AUF BILDSCHIRM 2101 : 2110 PRINT"{CLR}" 2120 FOR I=1 TO13:PRINTCHR$ (I+64)"="M%(I),CHR$ (I+13+64)"="M%(I+13):NEXT I 2130 END 2200 REM M%(1)-M%(26) ALS SEQFILE SPEICHERN 2201 : 2210 POKE 198,0:INPUT"{DOWN}FILENAME ";I$:I$=I$+",S,W" 2220 OPEN 2,8,2,I$ 2230 FOR I=1 TO26 2240 :PRINT#2,M%(I) 2250 NEXT I:CLOSE 2 2260 END 9000 REM ZEIGER POSITIONIEREN -------------------------------------------------- 9001 : 9010 HB%=SA/256:LB%=SA-HB%*256 9020 PRINT#15,"P";CHR$ (3);CHR$ (LB%);CHR$ (HB%);CHR$ (1) 9030 INPUT#15,A:IF A=50 THENPRINT"SATZNUMMER ZU HOCH" 9040 RETURN 9100 REM SATZ LESEN ------------------------------------------------------------ 9101 : 9110 IF A=50 THENRETURN 9120 INPUT#1,A$ 9130 RETURN
10 open1,8,3,"wortschatz":open15,8,15:rem wortschatz-datei oeffnen
20 data1,3,0,28,31,47,51,60,68,0,70,76,0,82,90,93,100,101,116,149,155,158
30 data178,0,0,189
35 data198 :rem buchstabe nach z !?!
40 dimin(26):fori=0to26:readin(i):nexti
50 gosub50000
60 print"sn="sn:print"ve="ve:print"o1="o1:print"o2="o2:print"ud="ud:print"ri="ri
70 print"ad="ad:print:goto50
50000 rem *****************************
50001 rem * *
50002 rem * wort-parser 4.0 *
50003 rem * *
50004 rem * <c> 1986 bei *
50005 rem * *
50006 rem * michael nickles *
50007 rem * *
50008 rem *****************************
50010 rem befehlssatzeingabe ---------------------------------------------------
50011 :
50012 ifud>0then50500:rem und
50015 sl=80: rem befehlssatzlaenge
50020 print"{down}":be$="":poke198,0:poke211,0:poke214,22:sys58732:print"{yel}{CBM-@}";
50030 getx$:ifpeek(203)=1then50120
50040 ifx$=""then50030
50050 iflen(be$)=0andasc(x$)=20then50030
50060 i=asc(x$):ifi<32ori>133andi<159thenifi<>20then50030
50070 iflen(be$)=slandi<>20then50030
50080 be$=be$+x$
50090 printchr$(20);x$;"{CBM-@}";
50100 ifi=20thenbe$=left$(be$,len(be$)-2):goto50030
50110 goto50030
50120 print"{left} "
50400 rem satzzeichen suchen ---------------------------------------------------
50401 :
50415 sn=0
50420 ifright$(be$,1)=" "thenbe$=left$(be$,len(be$)-1):goto50420
50430 i$=right$(be$,1)
50440 ifi$="."ori$="!"thensn=0:be$=left$(be$,len(be$)-1):goto50420
50450 ifi$="?"thensn=1:be$=left$(be$,len(be$)-1):goto50420
50500 rem be$-zeiger setzen ----------------------------------------------------
50501 :
50502 ifud=0thenve=0:ri=0:o1=0:o2=0
50504 ifud>0thenri=0:goto50530
50505 ifleft$(be$,1)=" "thenbe$=right$(be$,len(be$)-1):goto50505
50510 be$=be$+" "
50520 za=1 :rem zeiger a setzen
50530 zb=za+1
50540 ifmid$(be$,zb,1)=""thenud=0:return
50550 ifmid$(be$,zb,1)<>" "thenzb=zb+1:goto50550
50590 su$=mid$(be$,za,zb-za)
50600 rem su$ vorbehandlung ----------------------------------------------------
50601 :
50610 ifsu$="und"thenud=1:goto50630
50620 ifsu$=","thenud=2:goto50630
50625 goto50670
50630 za=zb+1
50640 ifmid$(be$,za,1)=""thenprint"Und was ?":goto50530
50650 ifmid$(be$,za,1)=" "thenza=za+1:goto50650
50660 return
50670 ifsu$="ihn"orsu$="sie"orsu$="es"theno1=om:goto51300
51000 rem binaere suchroutine --------------------------------------------------
51001 :
51007 rem anfang und ende des suchbereiches ermitteln
51008 i=asc(left$(su$,1))
51009 ifi-65<0ori-65>25thengosub51500:goto50000
51010 an=in(i-65):rem anfang des suchbereiches
51012 iz=64
51013 ifin(i-iz)=0theniz=iz-1:goto51013
51015 en=in(i-iz)-1:rem ende des suchbereiches
51016 ifan=0thengosub51500:ud=0:goto50000
51020 sz=197:n=int(log(en-an+1)/log(2))+1 :rem maximal-formel
51030 sa=an-1+(2^n)/2:rem mitte der ges.dat
51040 gosub52100:rem a$ lesen
51050 n=n-1 :rem 1.abfrage
51060 :
51070 rem su$ mit a$ vergleichen ------
51080 : iflen(su$)<3then51110
51090 : ifwa=1 and su$=left$(a$,len(su$)) thenve=wc:goto51300
51100 : ifwa<>3orsu$<>right$(a$,len(su$))then51110
51102 : ifud=1orud=2thenud=3:o1=0:o2=0
51104 : ifo1=0theno1=wc:om=o1:goto51300
51105 : ifo2=0theno2=wc:om=o1:goto51300
51110 : ifwa=2 and su$=a$ thenri=wc:goto51300
51115 : ifwa=5 and su$=a$ thenad=wc:goto51300
51120 : ifwa=1 and su$=a$ thenve=wc:goto51300
51125 : ifwa=4 and su$=a$ then51300
51130 : ifsu$<a$thensa=sa-(2^(n-1)):gosub52100:goto51160
51140 : ifsa+(2^(n-1))>sa+en-anandn>-1thenn=n-1:goto51140:rem ''ueberlauf''
51150 : sa=sa+(2^(n-1)):gosub52100:rem ''halbieren''
51160 :
51170 :n=n-1 :rem naechst. halbieren
51180 :ifn<0thengosub51500:ud=0:goto50000
51190 rem print"sa=";sa;" n=";n :rem unnoetig !!
51200 :goto51060:rem naechst. such-vers.
51300 rem naechstes wort
51310 za=zb+1
51320 ifmid$(be$,za,1)=""then50530
51330 ifmid$(be$,za,1)=" "thenza=za+1:goto51330
51350 goto50530
51500 rem ich kenne su$ nicht --------------------------------------------------
51501 :
51510 i=int(3*rnd(1))+1
51520 onigoto51530,51540,51550
51530 x$=""+"ich kenne das wort '"+su$+"' nicht.":goto52000
51540 x$=""+"das wort '"+su$+"' ist mir nicht bekannt.":goto52000
51550 x$=""+"deutsch ist leider nur meine zweite sprache. ich kenne das "
51555 x$=x$+"wort '"+su$+"' nicht.":goto52000
52000 rem formatierte textausgabe ----------------------------------------------
52010 ifx$=""then52090
52020 iflen(x$)<40thenprintx$:goto52090
52030 iflen(x$)=40thenprintx$;:goto52090
52040 i$=left$(x$,41):ic=0:fori=1tolen(i$):ifmid$(i$,i,1)=" "thenic=i
52050 nexti:ific=0thenprintx$:goto52090
52060 i$=left$(i$,ic-1):iflen(i$)=40thenprinti$;
52070 iflen(i$)<40thenprinti$
52080 x$=right$(x$,len(x$)-ic):goto52010
52090 return
52100 rem satz-teilbereich lesen -----------------------------------------------
52101 :
52110 hb%=sa/256:lb%=sa-hb%*256
52112 print#15,"p";chr$(3);chr$(lb%);chr$(hb%);chr$(1)
52114 input#15,a:ifa=50thenprint"satznummer zu hoch"
52120 ifa=50thenreturn
52130 input#1,a$
52140 wa=val(mid$(a$,22,1))
52150 wc=val(mid$(a$,23,3))
52160 a$=left$(a$,21)
52170 ifright$(a$,1)="{rght}"thena$=left$(a$,len(a$)-1):goto52170
52180 return
10 OPEN 1,8,3,"WORTSCHATZ":OPEN 15,8,15:REM WORTSCHATZ-DATEI OEFFNEN 20 DATA 1,3,0,28,31,47,51,60,68,0,70,76,0,82,90,93,100,101,116,149,155,158 30 DATA 178,0,0,189 35 DATA 198 :REM BUCHSTABE NACH Z !?! 40 DIM IN(26):FOR I=0 TO26:READ IN(I):NEXT I 50 GOSUB 50000 60 PRINT"SN="SN:PRINT"VE="VE:PRINT"O1="O1:PRINT"O2="O2:PRINT"UD="UD:PRINT"RI="RI 70 PRINT"AD="AD:PRINT:GOTO 50 50000 REM ***************************** 50001 REM * * 50002 REM * WORT-PARSER 4.0 * 50003 REM * * 50004 REM * <C> 1986 BEI * 50005 REM * * 50006 REM * MICHAEL NICKLES * 50007 REM * * 50008 REM ***************************** 50010 REM BEFEHLSSATZEINGABE --------------------------------------------------- 50011 : 50012 IF UD>0 THEN50500:REM UND 50015 SL=80: REM BEFEHLSSATZLAENGE 50020 PRINT"{DOWN}":BE$="":POKE 198,0:POKE 211,0:POKE 214,22:SYS 58732:PRINT"{YELLOW}@"; 50030 GET X$:IF PEEK (203)=1 THEN50120 50040 IF X$="" THEN50030 50050 IF LEN (BE$)=0AND ASC (X$)=20 THEN50030 50060 I=ASC (X$):IF I<32OR I>133AND I<159 THENIF I<>20 THEN50030 50070 IF LEN (BE$)=SLAND I<>20 THEN50030 50080 BE$=BE$+X$ 50090 PRINTCHR$ (20);X$;"@"; 50100 IF I=20 THENBE$=LEFT$ (BE$,LEN (BE$)-2):GOTO 50030 50110 GOTO 50030 50120 PRINT"{LEFT,SPACE}" 50400 REM SATZZEICHEN SUCHEN --------------------------------------------------- 50401 : 50415 SN=0 50420 IF RIGHT$ (BE$,1)=" " THENBE$=LEFT$ (BE$,LEN (BE$)-1):GOTO 50420 50430 I$=RIGHT$ (BE$,1) 50440 IF I$="."OR I$="!" THENSN=0:BE$=LEFT$ (BE$,LEN (BE$)-1):GOTO 50420 50450 IF I$="?" THENSN=1:BE$=LEFT$ (BE$,LEN (BE$)-1):GOTO 50420 50500 REM BE$-ZEIGER SETZEN ---------------------------------------------------- 50501 : 50502 IF UD=0 THENVE=0:RI=0:O1=0:O2=0 50504 IF UD>0 THENRI=0:GOTO 50530 50505 IF LEFT$ (BE$,1)=" " THENBE$=RIGHT$ (BE$,LEN (BE$)-1):GOTO 50505 50510 BE$=BE$+" " 50520 ZA=1 :REM ZEIGER A SETZEN 50530 ZB=ZA+1 50540 IF MID$ (BE$,ZB,1)="" THENUD=0:RETURN 50550 IF MID$ (BE$,ZB,1)<>" " THENZB=ZB+1:GOTO 50550 50590 SU$=MID$ (BE$,ZA,ZB-ZA) 50600 REM SU$ VORBEHANDLUNG ---------------------------------------------------- 50601 : 50610 IF SU$="UND" THENUD=1:GOTO 50630 50620 IF SU$="," THENUD=2:GOTO 50630 50625 GOTO 50670 50630 ZA=ZB+1 50640 IF MID$ (BE$,ZA,1)="" THENPRINT"UND WAS ?":GOTO 50530 50650 IF MID$ (BE$,ZA,1)=" " THENZA=ZA+1:GOTO 50650 50660 RETURN 50670 IF SU$="IHN"OR SU$="SIE"OR SU$="ES" THENO1=OM:GOTO 51300 51000 REM BINAERE SUCHROUTINE -------------------------------------------------- 51001 : 51007 REM ANFANG UND ENDE DES SUCHBEREICHES ERMITTELN 51008 I=ASC (LEFT$ (SU$,1)) 51009 IF I-65<0OR I-65>25 THENGOSUB 51500:GOTO 50000 51010 AN=IN(I-65):REM ANFANG DES SUCHBEREICHES 51012 IZ=64 51013 IF IN(I-IZ)=0 THENIZ=IZ-1:GOTO 51013 51015 EN=IN(I-IZ)-1:REM ENDE DES SUCHBEREICHES 51016 IF AN=0 THENGOSUB 51500:UD=0:GOTO 50000 51020 SZ=197:N=INT (LOG (EN-AN+1)/LOG (2))+1 :REM MAXIMAL-FORMEL 51030 SA=AN-1+(2^N)/2:REM MITTE DER GES.DAT 51040 GOSUB 52100:REM A$ LESEN 51050 N=N-1 :REM 1.ABFRAGE 51060 : 51070 REM SU$ MIT A$ VERGLEICHEN ------ 51080 : IF LEN (SU$)<3 THEN51110 51090 : IF WA=1 AND SU$=LEFT$ (A$,LEN (SU$)) THENVE=WC:GOTO 51300 51100 : IF WA<>3OR SU$<>RIGHT$ (A$,LEN (SU$)) THEN51110 51102 : IF UD=1OR UD=2 THENUD=3:O1=0:O2=0 51104 : IF O1=0 THENO1=WC:OM=O1:GOTO 51300 51105 : IF O2=0 THENO2=WC:OM=O1:GOTO 51300 51110 : IF WA=2 AND SU$=A$ THENRI=WC:GOTO 51300 51115 : IF WA=5 AND SU$=A$ THENAD=WC:GOTO 51300 51120 : IF WA=1 AND SU$=A$ THENVE=WC:GOTO 51300 51125 : IF WA=4 AND SU$=A$ THEN51300 51130 : IF SU$<A$ THENSA=SA-(2^(N-1)):GOSUB 52100:GOTO 51160 51140 : IF SA+(2^(N-1))>SA+EN-ANAND N>-1 THENN=N-1:GOTO 51140:REM ''UEBERLAUF'' 51150 : SA=SA+(2^(N-1)):GOSUB 52100:REM ''HALBIEREN'' 51160 : 51170 :N=N-1 :REM NAECHST. HALBIEREN 51180 :IF N<0 THENGOSUB 51500:UD=0:GOTO 50000 51190 REM PRINT"SA=";SA;" N=";N :REM UNNOETIG !! 51200 :GOTO 51060:REM NAECHST. SUCH-VERS. 51300 REM NAECHSTES WORT 51310 ZA=ZB+1 51320 IF MID$ (BE$,ZA,1)="" THEN50530 51330 IF MID$ (BE$,ZA,1)=" " THENZA=ZA+1:GOTO 51330 51350 GOTO 50530 51500 REM ICH KENNE SU$ NICHT -------------------------------------------------- 51501 : 51510 I=INT (3*RND (1))+1 51520 ON IGOTO 51530,51540,51550 51530 X$=""+"ICH KENNE DAS WORT '"+SU$+"' NICHT.":GOTO 52000 51540 X$=""+"DAS WORT '"+SU$+"' IST MIR NICHT BEKANNT.":GOTO 52000 51550 X$=""+"DEUTSCH IST LEIDER NUR MEINE ZWEITE SPRACHE. ICH KENNE DAS " 51555 X$=X$+"WORT '"+SU$+"' NICHT.":GOTO 52000 52000 REM FORMATIERTE TEXTAUSGABE ---------------------------------------------- 52010 IF X$="" THEN52090 52020 IF LEN (X$)<40 THENPRINTX$:GOTO 52090 52030 IF LEN (X$)=40 THENPRINTX$;:GOTO 52090 52040 I$=LEFT$ (X$,41):IC=0:FOR I=1 TOLEN (I$):IF MID$ (I$,I,1)=" " THENIC=I 52050 NEXT I:IF IC=0 THENPRINTX$:GOTO 52090 52060 I$=LEFT$ (I$,IC-1):IF LEN (I$)=40 THENPRINTI$; 52070 IF LEN (I$)<40 THENPRINTI$ 52080 X$=RIGHT$ (X$,LEN (X$)-IC):GOTO 52010 52090 RETURN 52100 REM SATZ-TEILBEREICH LESEN ----------------------------------------------- 52101 : 52110 HB%=SA/256:LB%=SA-HB%*256 52112 PRINT#15,"P";CHR$ (3);CHR$ (LB%);CHR$ (HB%);CHR$ (1) 52114 INPUT#15,A:IF A=50 THENPRINT"SATZNUMMER ZU HOCH" 52120 IF A=50 THENRETURN 52130 INPUT#1,A$ 52140 WA=VAL (MID$ (A$,22,1)) 52150 WC=VAL (MID$ (A$,23,3)) 52160 A$=LEFT$ (A$,21) 52170 IF RIGHT$ (A$,1)="{RIGHT}" THENA$=LEFT$ (A$,LEN (A$)-1):GOTO 52170 52180 RETURN
10 rem *****************************
20 rem * ram-editor *
30 rem * <c> 1985 bei *
40 rem * michael nickles *
50 rem *****************************
60 dimm$(255),wz(26)
70 goto51000
8000 rem ordnungsfile index-seq ------------------------------------------------
8005 print"moechten sie einen index-file erstellen?";
8006 geti$:ifi$<>"j"andi$<>"n"then8006
8008 ifi$="n"thenreturn
8019 input"ab speicherstelle ";e1
8020 input"bis ";e2:ife1>e2thenprint"unsinn!:goto8005
8030 input"schrittweite-step ";sw
8100 print"{down}bitte warten!"
8105 fori1=e1toe2stepsw
8110 :fori2=1to26
8112 : ifusr(i1)=64+i2andwz(i2)=0thenwz(i2)=i1
8114 :nexti2
8120 nexti1
8400 rem wz(n) als seq-file speichern ------------------------------------------
8410 poke198,0:input"filename ";i$:i$=i$+",s,w"
8420 open2,8,2,i$
8430 fori=1to26
8440 :print#2,wz(i)
8450 nexti:close2
8500 return
9000 rem up-positionieren ------------------------------------------------------
9010 hb%=sa/256:lb%=sa-hb%*256
9020 print#15,"p";chr$(3);chr$(lb%);chr$(hb%);chr$(1)
9030 input#15,a:ifa=50thenprint"satznummer zu hoch!":end
9040 return
9100 rem up-satz lesen ---------------------------------------------------------
9110 ifa=50thenreturn
9120 input#1,a$
9130 return
51000 rem ram-editor -----------------------------------------------------------
51029 poke53280,0:poke53281,0:print"{wht}"
51030 print"{clr}{rvon}ram-editor v. 1.5 von michael nickles{rvof}";
51040 rem modifizierte peek-funktion -------------------------------------------
51045 data32,247,183,120,169,52,133,1
51048 data160,0,177,20,168,169,55,133
51050 data1,88,76,162,179
51060 fori=704to724:readx:pokei,x:s=s+x:nexti
51070 poke785,192:poke786,2:rem usr-zeiger
51080 ifs<>2325thenprint"fehler in datas{$a0}!":end
51130 rem anfang ---------------------------------------------------------------
51131 rem goto8000
51132 print"{down}ram editor kann in freien ram-bereichen";
51134 print"relative dateien verwalten."
51135 print"bitte name der relativen datei angeben,
51136 print"von der daten ins ram geschrieben werden";
51137 print"sollen (disk einlegen)!"
51138 input"{down}filenname ";f$
51139 open15,8,15 :rem floppykanal an
51140 print#15,"i0":rem initializieren
51141 open1,8,3,f$ :rem datei oeffnen
51143 print"bitte geben sie anfang und ende des bereiches an, der ins ram";
51144 print" geschrieben werden soll!"
51146 poke198,0:input"{down}anfang";an
51147 input"ende ";en:ifen<=anthenprint"unsinn !":goto51146
51148 sa=an:gosub9000:gosub9100
51149 print"{down}bitte geben sie anfang und ende des ram-bereiches an, in dem";
51150 print" die daten abgelegt werden sollen! achtung - diese angaben ";
51151 print"werden nicht ueberprueft !
51152 poke198,0:input"{down}anfang";e1
51153 input"ende";e2
51154 ife2<e1thenprint"unsinn !":goto51152run
51159 gosub60000
51200 rem ins ram schreiben ----------
51205 print"{down}bitte warten!"
51210 iz=e1
51215 forsa=antoen:gosub9000:gosub9100: rem lese a$
51220 forii=1to3
51225 :ifma(ii)=0then51250
51230 :ifty%(ii)=2thengosub51500:rem $
51240 :ifty%(ii)=1thengosub51600:rem val
51250 nextii
51255 nextsa
51270 rem speichern auf disk ---------
51280 print"{down}bitte diskette einlegen auf der ram-
51281 print"bereich gespeichert wird! [return]":poke198,0:wait198,1
51290 poke198,0:input"filename";i$:ifi$=""then51290
51300 print"{down}bitte warten!"
51315 fori=1to15:closei:next
51320 open1,8,1,i$
51330 hb%=int(e1/256):lb%=e1-hb%*256
51340 print#1,chr$(lb%);chr$(hb%);
51350 fori=e1toe2
51360 :print#1,chr$(usr(i));
51370 nexti:close1
51375 gosub8000 :rem index-seq-file
51380 print"*** fertig ***":end
51500 rem als string speichern ------
51501 iflen(a$)<mb(ii)-ma(ii)+1thena$=a$+" ":goto51501
51510 fori=ma(ii)tomb(ii)
51515 :ifasc(mid$(a$,i,1))=29thenpokeiz,32:iz=iz+1:goto51520
51516 :pokeiz,asc(mid$(a$,i,1)):iz=iz+1
51520 nexti
51550 return
51600 rem als val-wert speichern ------
51610 poke iz,val(mid$(a$,ma(ii),mb(ii)))
51620 iz=iz+1
51630 return
60000 rem markieren wie datiert wird
60010 i$="":fori=1tolen(a$)
60012 : ifasc(mid$(a$,i,1))=29theni$=i$+" ":goto60015
60014 : i$=i$+mid$(a$,i,1)
60015 nexti
60019 fori=1tolen(i$):m$(i)="0":next
60020 im=1:print
60025 printi$:fori=1tolen(i$):printm$(i);:next:print
60030 print"{down}edit >";mid$(i$,im,1);"< {rvon}"im"{rvof}"
60035 i=peek(203):ifi=64then60035
60040 ifi=2andpeek(653)=0andim<>len(i$)thenim=im+1:goto60070
60042 ifi=2andpeek(653)=1andim<>1thenim=im-1:goto60070
60045 ifi=56thenm$(im)="1":goto60070
60047 ifi=59andig=0thenm$(im)="2":goto60070
60050 ifi=8andig=0thenm$(im)="3":goto60070
60055 ifi=35thenm$(im)="0":goto60070
60056 ifi=1then60100
60060 goto60035
60070 print"{up} {left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{left}{up}{up}{up}";:goto60025
60100 rem umrechnen
60102 fori=1to3:ma(i)=0:mb(i)=0:nexti
60105 forii=1to3:rem print"{rvon}ii={rvof}";ii
60110 :fori=1tolen(i$):ifma(ii)=0andm$(i)=right$(str$(ii),1)thenma(ii)=i
60112 :remprint"m$(i)=";m$(i);" right$"right$(sth$(ii),1)
60113 nexti
60115 :ifma(ii)=0then60150
60120 : mb(ii)=ma(ii)
60125 : mb(ii)=mb(ii)+1:remprint,mb(ii)
60130 : ifm$(mb(ii))<>right$(str$(ii),1)thenmb(ii)=mb(ii)-1:goto60150
60135 : ifmb(ii)=len(i$)then60150
60140 : goto60125
60150 nextii
60200 print"{clr}{rvon}";i$:print"{rvof}wurde in folgende bereiche zerlegt:{down}"
60210 fori=1to3
60215 ifma(i)=0then60230
60220 print"bereich";i;": {rvon}"mid$(i$,ma(i),mb(i)-ma(i)+1)"{rvof}"
60230 nexti:print
60240 print"bitte geben sie nun an, ob die bereiche
60245 print"als {rvon}v{rvof}al-wert oder als {rvon}s{rvof}tring
60250 print"gespeichert werden sollen!{down}"
60260 fori=1to3
60262 ifma(i)=0then60290
60265 print"{rvon}"mid$(i$,ma(i),mb(i)-ma(i)+1)"{rvof}"
60270 poke198,0:input"{rvon}v{rvof} oder {rvon}s{rvof}";ii$
60275 ifii$<>"v"andii$<>"s"then60270
60280 ty%(i)=1:ifii$="s"thenty%(i)=2
60290 nexti
60300 return
61000 close1:open1,8,15
61010 input#1,a,b$,c,d
61020 printa,b$,c,d
61030 close1
10 REM ***************************** 20 REM * RAM-EDITOR * 30 REM * <C> 1985 BEI * 40 REM * MICHAEL NICKLES * 50 REM ***************************** 60 DIM M$(255),WZ(26) 70 GOTO 51000 8000 REM ORDNUNGSFILE INDEX-SEQ ------------------------------------------------ 8005 PRINT"MOECHTEN SIE EINEN INDEX-FILE ERSTELLEN?"; 8006 GET I$:IF I$<>"J"AND I$<>"N" THEN8006 8008 IF I$="N" THENRETURN 8019 INPUT"AB SPEICHERSTELLE ";E1 8020 INPUT"BIS{15SPACE}";E2:IF E1>E2 THENPRINT"UNSINN!:GOTO8005 8030 INPUT"SCHRITTWEITE-STEP ";SW 8100 PRINT"{DOWN}BITTE WARTEN!" 8105 FOR I1=E1 TOE2STEP SW 8110 :FOR I2=1 TO26 8112 : IF USR (I1)=64+I2AND WZ(I2)=0 THENWZ(I2)=I1 8114 :NEXT I2 8120 NEXT I1 8400 REM WZ(N) ALS SEQ-FILE SPEICHERN ------------------------------------------ 8410 POKE 198,0:INPUT"FILENAME ";I$:I$=I$+",S,W" 8420 OPEN 2,8,2,I$ 8430 FOR I=1 TO26 8440 :PRINT#2,WZ(I) 8450 NEXT I:CLOSE 2 8500 RETURN 9000 REM UP-POSITIONIEREN ------------------------------------------------------ 9010 HB%=SA/256:LB%=SA-HB%*256 9020 PRINT#15,"P";CHR$ (3);CHR$ (LB%);CHR$ (HB%);CHR$ (1) 9030 INPUT#15,A:IF A=50 THENPRINT"SATZNUMMER ZU HOCH!":END 9040 RETURN 9100 REM UP-SATZ LESEN --------------------------------------------------------- 9110 IF A=50 THENRETURN 9120 INPUT#1,A$ 9130 RETURN 51000 REM RAM-EDITOR ----------------------------------------------------------- 51029 POKE 53280,0:POKE 53281,0:PRINT"{WHITE}" 51030 PRINT"{CLR,RVSON}RAM-EDITOR V. 1.5{4SPACE}VON MICHAEL NICKLES{RVOFF}"; 51040 REM MODIFIZIERTE PEEK-FUNKTION ------------------------------------------- 51045 DATA 32,247,183,120,169,52,133,1 51048 DATA 160,0,177,20,168,169,55,133 51050 DATA 1,88,76,162,179 51060 FOR I=704 TO724:READ X:POKE I,X:S=S+X:NEXT I 51070 POKE 785,192:POKE 786,2:REM USR-ZEIGER 51080 IF S<>2325 THENPRINT"FEHLER IN DATAS{SHIFT-SPACE}!":END 51130 REM ANFANG --------------------------------------------------------------- 51131 REM GOTO8000 51132 PRINT"{DOWN}RAM EDITOR{SPACE,SPACE}KANN IN FREIEN RAM-BEREICHEN"; 51134 PRINT"RELATIVE DATEIEN VERWALTEN." 51135 PRINT"BITTE NAME DER RELATIVEN DATEI ANGEBEN, 51136 PRINT"VON DER DATEN INS RAM GESCHRIEBEN WERDEN"; 51137 PRINT"SOLLEN (DISK EINLEGEN)!" 51138 INPUT"{DOWN}FILENNAME ";F$ 51139 OPEN 15,8,15 :REM FLOPPYKANAL AN 51140 PRINT#15,"I0":REM INITIALIZIEREN 51141 OPEN 1,8,3,F$ :REM DATEI OEFFNEN 51143 PRINT"BITTE GEBEN SIE ANFANG UND ENDE DES{5SPACE}BEREICHES AN, DER INS RAM"; 51144 PRINT" GESCHRIEBEN{3SPACE}WERDEN SOLL!" 51146 POKE 198,0:INPUT"{DOWN}ANFANG";AN 51147 INPUT"ENDE{2SPACE}";EN:IF EN<=AN THENPRINT"UNSINN !":GOTO 51146 51148 SA=AN:GOSUB 9000:GOSUB 9100 51149 PRINT"{DOWN}BITTE GEBEN SIE ANFANG UND ENDE DES{SPACE,4SPACE}RAM-BEREICHES AN, IN DEM"; 51150 PRINT" DIE DATEN{6SPACE}ABGELEGT WERDEN SOLLEN! ACHTUNG - DIESE ANGABEN "; 51151 PRINT"WERDEN NICHT UEBERPRUEFT ! 51152 POKE 198,0:INPUT"{DOWN}ANFANG";E1 51153 INPUT"ENDE";E2 51154 IF E2<E1 THENPRINT"UNSINN !":GOTO 51152RUN 51159 GOSUB 60000 51200 REM INS RAM SCHREIBEN ---------- 51205 PRINT"{DOWN}BITTE WARTEN!" 51210 IZ=E1 51215 FOR SA=AN TOEN:GOSUB 9000:GOSUB 9100: REM LESE A$ 51220 FOR II=1 TO3 51225 :IF MA(II)=0 THEN51250 51230 :IF TY%(II)=2 THENGOSUB 51500:REM $ 51240 :IF TY%(II)=1 THENGOSUB 51600:REM VAL 51250 NEXT II 51255 NEXT SA 51270 REM SPEICHERN AUF DISK --------- 51280 PRINT"{DOWN}BITTE DISKETTE EINLEGEN AUF DER RAM- 51281 PRINT"BEREICH GESPEICHERT WIRD! [RETURN]":POKE 198,0:WAIT 198,1 51290 POKE 198,0:INPUT"FILENAME";I$:IF I$="" THEN51290 51300 PRINT"{DOWN}BITTE WARTEN!" 51315 FOR I=1 TO15:CLOSE I:NEXT 51320 OPEN 1,8,1,I$ 51330 HB%=INT (E1/256):LB%=E1-HB%*256 51340 PRINT#1,CHR$ (LB%);CHR$ (HB%); 51350 FOR I=E1 TOE2 51360 :PRINT#1,CHR$ (USR (I)); 51370 NEXT I:CLOSE 1 51375 GOSUB 8000 :REM INDEX-SEQ-FILE 51380 PRINT"*** FERTIG ***":END 51500 REM ALS STRING SPEICHERN ------ 51501 IF LEN (A$)<MB(II)-MA(II)+1 THENA$=A$+" ":GOTO 51501 51510 FOR I=MA(II) TOMB(II) 51515 :IF ASC (MID$ (A$,I,1))=29 THENPOKE IZ,32:IZ=IZ+1:GOTO 51520 51516 :POKE IZ,ASC (MID$ (A$,I,1)):IZ=IZ+1 51520 NEXT I 51550 RETURN 51600 REM ALS VAL-WERT SPEICHERN ------ 51610 POKE IZ,VAL (MID$ (A$,MA(II),MB(II))) 51620 IZ=IZ+1 51630 RETURN 60000 REM MARKIEREN WIE DATIERT WIRD 60010 I$="":FOR I=1 TOLEN (A$) 60012 : IF ASC (MID$ (A$,I,1))=29 THENI$=I$+" ":GOTO 60015 60014 : I$=I$+MID$ (A$,I,1) 60015 NEXT I 60019 FOR I=1 TOLEN (I$):M$(I)="0":NEXT 60020 IM=1:PRINT 60025 PRINTI$:FOR I=1 TOLEN (I$):PRINTM$(I);:NEXT :PRINT 60030 PRINT"{DOWN}EDIT{SPACE,2SPACE}>";MID$ (I$,IM,1);"<{4SPACE,RVSON}"IM"{RVOFF}" 60035 I=PEEK (203):IF I=64 THEN60035 60040 IF I=2AND PEEK (653)=0AND IM<>LEN (I$) THENIM=IM+1:GOTO 60070 60042 IF I=2AND PEEK (653)=1AND IM<>1 THENIM=IM-1:GOTO 60070 60045 IF I=56 THENM$(IM)="1":GOTO 60070 60047 IF I=59AND IG=0 THENM$(IM)="2":GOTO 60070 60050 IF I=8AND IG=0 THENM$(IM)="3":GOTO 60070 60055 IF I=35 THENM$(IM)="0":GOTO 60070 60056 IF I=1 THEN60100 60060 GOTO 60035 60070 PRINT"{UP,25SPACE,25LEFT,3UP}";:GOTO 60025 60100 REM UMRECHNEN 60102 FOR I=1 TO3:MA(I)=0:MB(I)=0:NEXT I 60105 FOR II=1 TO3:REM PRINT"{RVSON}II={RVOFF}";II 60110 :FOR I=1 TOLEN (I$):IF MA(II)=0AND M$(I)=RIGHT$ (STR$ (II),1) THENMA(II)=I 60112 :REM PRINT"M$(I)=";M$(I);"{2SPACE}RIGHT$"RIGHT$(STH$(II),1) 60113 NEXT I 60115 :IF MA(II)=0 THEN60150 60120 : MB(II)=MA(II) 60125 : MB(II)=MB(II)+1:REM PRINT,MB(II) 60130 : IF M$(MB(II))<>RIGHT$ (STR$ (II),1) THENMB(II)=MB(II)-1:GOTO 60150 60135 : IF MB(II)=LEN (I$) THEN60150 60140 : GOTO 60125 60150 NEXT II 60200 PRINT"{CLR,RVSON}";I$:PRINT"{RVOFF}WURDE IN FOLGENDE BEREICHE ZERLEGT:{DOWN}" 60210 FOR I=1 TO3 60215 IF MA(I)=0 THEN60230 60220 PRINT"BEREICH";I;":{SPACE,RVSON}"MID$ (I$,MA(I),MB(I)-MA(I)+1)"{RVOFF}" 60230 NEXT I:PRINT 60240 PRINT"BITTE GEBEN SIE NUN AN, OB DIE BEREICHE 60245 PRINT"ALS{SPACE,RVSON}V{RVOFF}AL-WERT ODER ALS{SPACE,RVSON}S{RVOFF}TRING 60250 PRINT"GESPEICHERT WERDEN SOLLEN!{DOWN}" 60260 FOR I=1 TO3 60262 IF MA(I)=0 THEN60290 60265 PRINT"{RVSON}"MID$ (I$,MA(I),MB(I)-MA(I)+1)"{RVOFF}" 60270 POKE 198,0:INPUT"{RVSON}V{RVOFF,SPACE}ODER{SPACE,RVSON}S{RVOFF}";II$ 60275 IF II$<>"V"AND II$<>"S" THEN60270 60280 TY%(I)=1:IF II$="S" THENTY%(I)=2 60290 NEXT I 60300 RETURN 61000 CLOSE 1:OPEN 1,8,15 61010 INPUT#1,A,B$,C,D 61020 PRINTA,B$,C,D 61030 CLOSE 1
10 ifi=0theni=1:load"wortschatz",8,1
20 dim be$(25)
30 dimin(26):fori=1to26:readin(i):nexti
40 data40960,41006,0,41581,41650
50 data42018,42110,42317,42501,0
60 data42547,42685,0,42823,43007
70 data43076,43237,43260,43605,44364
80 data44502,44571,45031,0,0,45284
100 rem modifizierte peek-funktion ---------------------------------------------
101 :
110 data32,247,183,120,169,52,133,1
120 data160,0,177,20,168,169,55,133
130 data1,88,76,162,179
140 fori=704to724:readx:pokei,x:s=s+x:nexti
150 poke785,192:poke786,2:rem usr-zeiger
160 ifs<>2325thenprint"fehler in datas{$a0}!":end
200 gosub50000
210 print"ve="ve:print"o1="o1:print"o2="o2:print"ad="ad:print"ri="ri
230 goto200
50000 rem *************************
50001 rem * *
50002 rem * wort-parser 5.0 *
50003 rem * *
50004 rem * <c> 1986 bei *
50005 rem * *
50006 rem * michael nickles *
50007 rem * *
50008 rem *************************
50010 rem befehlseingabe -------------------------------------------------------
50011 :
50015 j1=1:j2=10:j3=198:j4=54276:j5=54277:j6=203:j7=3000:j8=2:j9=20:j0=54273
50016 k1=54272:k2=89:k3=129:k4=47:k5=57:k6=65:k7=90:k8=32:k9=34:k0=44
50017 print
50019 poke54296,15:rem und abfragen
50020 tt=.: rem input-interrupt-zaehler
50030 fori=j1toj2:be$(i)="":next:pokej3,.:be$="": rem satz loeschen
50040 print"{yel}>";:print"{CBM-@}";
50050 pokej4,.:pokej5,.:getx$:ii=peek(j6):iftt<j7then50060:rem tt=tt+1 !!!!
50055 fori=1tolen(be$)+j8:printchr$(j9);:next:ve=0:return
50060 ifx$=""then50050
50061 pokej5,j1:pokej0,j2:pokek1,k2:pokej4,k3
50070 tt=0
50080 ifii=1thenifbe$<>""thenprint"{left}.":goto50200
50085 i=asc(x$):ifi>k4andi<k5then50100
50090 i=asc(x$):ifi<k6ori>k7thenifi<>k8andi<>j9andi<>k9andi<>k0then50050
50100 ifi=j9andlen(be$)=.then50050
50110 ifi=j9thenprint"{left}{left} {left}{left}{CBM-@}";:be$=left$(be$,len(be$)-j1):goto50050
50120 be$=be$+chr$(i):iflen(be$)>78thenbe$=left$(be$,78):goto50050
50125 ifi=k9theni=64
50130 print"{left}";chr$(i);"{CBM-@}";:goto50050
50200 rem codierung ------------------------------------------------------------
50201 :
50250 rem wort im ram suchen *******
50254 i$=be$
50255 im=asc(left$(i$,1)):i=in(im-64)
50262 rem begin loop <<<
50264 :if im<>usr(i)then 50280
50265 :fori1=1tolen(i$):printmid$(i$,i1,1),chr$(usr(i+i1-1))
50270 : ifasc(mid$(i$,i1,1))<>usr(i+i1-1)theni1=len(i$):goto50276
50273 : ifi1=len(i$)andusr(i+21)=3thenob=usr(i+22):om=ob:print"objekt="ob:end
50274 : ifi1=len(i$)andusr(i+21)=2thenri=usr(i+22):print"richtung="ri:end
50275 : ifi1=len(i$)andusr(i+21)=1thenve=usr(i+22):print"verb="ve:end
50276 :nexti1
50278 i=i+23:goto50264
50280 rem end loop <<<
50290 print"ich kenne das wort "be$" nicht !":end
10 IF I=0 THENI=1:LOAD "WORTSCHATZ",8,1 20 DIM BE$(25) 30 DIM IN(26):FOR I=1 TO26:READ IN(I):NEXT I 40 DATA 40960,41006,0,41581,41650 50 DATA 42018,42110,42317,42501,0 60 DATA 42547,42685,0,42823,43007 70 DATA 43076,43237,43260,43605,44364 80 DATA 44502,44571,45031,0,0,45284 100 REM MODIFIZIERTE PEEK-FUNKTION --------------------------------------------- 101 : 110 DATA 32,247,183,120,169,52,133,1 120 DATA 160,0,177,20,168,169,55,133 130 DATA 1,88,76,162,179 140 FOR I=704 TO724:READ X:POKE I,X:S=S+X:NEXT I 150 POKE 785,192:POKE 786,2:REM USR-ZEIGER 160 IF S<>2325 THENPRINT"FEHLER IN DATAS{SHIFT-SPACE}!":END 200 GOSUB 50000 210 PRINT"VE="VE:PRINT"O1="O1:PRINT"O2="O2:PRINT"AD="AD:PRINT"RI="RI 230 GOTO 200 50000 REM ************************* 50001 REM * * 50002 REM * WORT-PARSER 5.0 * 50003 REM * * 50004 REM * <C> 1986 BEI * 50005 REM * * 50006 REM * MICHAEL NICKLES * 50007 REM * * 50008 REM ************************* 50010 REM BEFEHLSEINGABE ------------------------------------------------------- 50011 : 50015 J1=1:J2=10:J3=198:J4=54276:J5=54277:J6=203:J7=3000:J8=2:J9=20:J0=54273 50016 K1=54272:K2=89:K3=129:K4=47:K5=57:K6=65:K7=90:K8=32:K9=34:K0=44 50017 PRINT 50019 POKE 54296,15:REM UND ABFRAGEN 50020 TT=.: REM INPUT-INTERRUPT-ZAEHLER 50030 FOR I=J1 TOJ2:BE$(I)="":NEXT :POKE J3,.:BE$="": REM SATZ LOESCHEN 50040 PRINT"{YELLOW}>";:PRINT"@"; 50050 POKE J4,.:POKE J5,.:GET X$:II=PEEK (J6):IF TT<J7 THEN50060:REM TT=TT+1 !!!! 50055 FOR I=1 TOLEN (BE$)+J8:PRINTCHR$ (J9);:NEXT :VE=0:RETURN 50060 IF X$="" THEN50050 50061 POKE J5,J1:POKE J0,J2:POKE K1,K2:POKE J4,K3 50070 TT=0 50080 IF II=1 THENIF BE$<>"" THENPRINT"{LEFT}.":GOTO 50200 50085 I=ASC (X$):IF I>K4AND I<K5 THEN50100 50090 I=ASC (X$):IF I<K6OR I>K7 THENIF I<>K8AND I<>J9AND I<>K9AND I<>K0 THEN50050 50100 IF I=J9AND LEN (BE$)=. THEN50050 50110 IF I=J9 THENPRINT"{2LEFT,2SPACE,2LEFT}@";:BE$=LEFT$ (BE$,LEN (BE$)-J1):GOTO 50050 50120 BE$=BE$+CHR$ (I):IF LEN (BE$)>78 THENBE$=LEFT$ (BE$,78):GOTO 50050 50125 IF I=K9 THENI=64 50130 PRINT"{LEFT}";CHR$ (I);"@";:GOTO 50050 50200 REM CODIERUNG ------------------------------------------------------------ 50201 : 50250 REM WORT IM RAM SUCHEN ******* 50254 I$=BE$ 50255 IM=ASC (LEFT$ (I$,1)):I=IN(IM-64) 50262 REM BEGIN LOOP <<< 50264 :IF IM<>USR (I) THEN 50280 50265 :FOR I1=1 TOLEN (I$):PRINTMID$ (I$,I1,1),CHR$ (USR (I+I1-1)) 50270 : IF ASC (MID$ (I$,I1,1))<>USR (I+I1-1) THENI1=LEN (I$):GOTO 50276 50273 : IF I1=LEN (I$)AND USR (I+21)=3 THENOB=USR (I+22):OM=OB:PRINT"OBJEKT="OB:END 50274 : IF I1=LEN (I$)AND USR (I+21)=2 THENRI=USR (I+22):PRINT"RICHTUNG="RI:END 50275 : IF I1=LEN (I$)AND USR (I+21)=1 THENVE=USR (I+22):PRINT"VERB="VE:END 50276 :NEXT I1 50278 I=I+23:GOTO 50264 50280 REM END LOOP <<< 50290 PRINT"ICH KENNE DAS WORT "BE$" NICHT !":END