MIDGARDAudhumbla 0.7

Kapitel 6: Kontrollstrukturen & Schleifen

In diesem Kapitel lernst du, wie dein Programm Entscheidungen trifft und Dinge mehrfach ausführt. Kontrollstrukturen bestimmen den Programmfluss – also wann, wie oft und unter welchen Bedingungen Code ausgeführt wird. Dieses Wissen ist absolut grundlegend für alles Weitere im LPC.

Weiter zu Kapitel 7

Danach: Arrays und Mappings im Detail.

6.1 Was sind Kontrollstrukturen?

Normalerweise wird Code von oben nach unten ausgeführt — Zeile für Zeile. Das ist die einfachste Form eines Programms: Schritt 1, dann Schritt 2, dann Schritt 3. Aber so ein „immer-gleich-ablaufendes“ Programm ist langweilig: Es kann auf nichts reagieren, nichts wiederholen, nicht auf unterschiedliche Situationen unterschiedlich antworten.

Kontrollstrukturen sind die Werkzeuge, mit denen du diesen starren Ablauf veränderst. Sie sind das, was Programmieren erst lebendig macht. Ohne sie könnte ein Item zwar Text ausgeben, aber nicht entscheiden, ob ein Spieler Level 10 hat oder nicht — es würde immer denselben Text liefern.

Du kannst damit:

  • Entscheidungen treffen — if, else: „Wenn der Spieler Level 10 hat, dann lass ihn ein.“
  • Code mehrfach ausführenwhile, for, foreach: „Für jeden Spieler im Raum: gib einen Text aus.“
  • Unterschiedliche Fälle behandeln — switch: „Je nach Würfelwurf passiert etwas anderes.“
  • Schleifen gezielt verlassenbreak, continue: „Suche, bis du den richtigen Spieler findest, dann hör auf.“
  • Fehler abfangencatch: „Versuche das, aber crashe nicht, falls es schiefgeht.“

Ohne Kontrollstrukturen wäre LPC extrem eingeschränkt — eigentlich überhaupt nicht zum Programmieren brauchbar. Sie sind das Rückgrat jeder Spielmechanik und jeder Reaktion auf Spieleraktionen.

6.2 Die if-Anweisung – Entscheidungen treffen

Die if-Anweisung ist die einfachste und häufigste Kontrollstruktur. Sie prüft eine Bedingung — also einen Ausdruck, der entweder „wahr“ oder „falsch“ ist — und führt einen Codeblock nur dann aus, wenn die Bedingung wahr ist.


if (x > 10)
{
  write("x ist größer als 10.\n");
}
      

Lies das laut: „Wenn x größer als 10 ist, dann gib den Text aus.“ Wenn x 5 ist, passiert nichts. Wenn x 11 ist, passiert die Ausgabe.

Wahr und Falsch in LPC

Anders als manche Sprachen hat LPC keinen separaten bool-Typ. Stattdessen gilt eine einfache Regel:

  • 0 bedeutet falsch
  • Jeder andere Wert bedeutet wahr

Das gilt für alle Typen: Eine Zahl ungleich 0 ist „wahr“, ein nicht-leerer String ist „wahr“, ein vorhandenes Objekt ist „wahr“. Ein zerstörtes Objekt zeigt sich als 0 und ist damit „falsch“. Daher der typische Test:


if (pl) {
  // pl ist nicht 0, also haben wir ein Objekt
  write(pl->Name(WER) + "\n");
}

if (!pl) {
  // pl ist 0
  write("Niemand da.\n");
}
      

Bedingungen können beliebig kompliziert sein


if (this_player()->QueryProp(P_LEVEL) >= 10)         // einfacher Vergleich
if (hp > 0 && hp < 50)                               // mit UND
if (race == "Elf" || race == "Mensch")               // mit ODER
if (!present("schluessel", this_player()))           // mit Negation
      

6.3 if ohne geschweifte Klammern

Besteht der Codeblock nur aus einer einzigen Anweisung, kannst du die geschweiften Klammern weglassen. Das ist syntaktisch erlaubt und wird in vielen LPC-Beispielen so geschrieben:


if (hp <= 0)
  die();

// Ist äquivalent zu:
if (hp <= 0)
{
  die();
}
      

Achtung: Die Falle der weggelassenen Klammern

Wenn du später eine zweite Zeile hinzufügst, gehört sie nicht automatisch zum if-Block — auch wenn die Einrückung das suggeriert. Klassischer Fehler:


if (hp <= 0)
  die();
  log_file("debug", "Spieler ist gestorben\n");
// Die log_file-Zeile wird IMMER ausgeführt, egal ob hp ≤ 0 oder nicht!
      

Mit Klammern wäre das eindeutig:


if (hp <= 0) {
  die();
  log_file("debug", "Spieler ist gestorben\n");
}
      

Anfänger-Tipp: Nutze Klammern fast immer — auch bei einzeiligen Blöcken. Das ist eine billige Versicherung gegen einen sehr ärgerlichen Bug, der oft erst Wochen später beim Erweitern auffällt.

6.4 if und else

Mit else definierst du, was passiert, wenn die Bedingung nicht erfüllt ist. Damit hast du eine Weichenstellung: Genau einer der beiden Zweige wird ausgeführt — nie beide, nie keiner.


if (level >= 10)
{
  write("Du darfst eintreten.\n");
}
else
{
  write("Du bist noch zu schwach.\n");
}
      

Im MUD-Code ist if/else einer der häufigsten Bausteine — überall, wo ein Objekt auf eine Spieleraktion reagiert, steckt mindestens ein if/else drin: Hat der Spieler die Quest schon gelöst? Hat er den richtigen Schlüssel? Ist der Raum erleuchtet?

Verschachtelte if/else

Du kannst if/else auch in andere if/else-Blöcke schachteln — das wird aber schnell unübersichtlich:


if (pl) {
  if (pl->QueryProp(P_LEVEL) >= 10) {
    write("Du darfst eintreten.\n");
  } else {
    write("Du bist noch zu schwach.\n");
  }
} else {
  write("Niemand da.\n");
}
      

Ab drei oder mehr Schachtelebenen wird das schnell unübersichtlich. Nutze dann else if (siehe nächster Abschnitt) oder Frühausstiege mit return.

6.5 Mehrere Bedingungen: else if

Für mehr als zwei Fälle nutzt man else if. Das ist eine Kette von Bedingungen, die der Reihe nach geprüft werden. Sobald eine Bedingung zutrifft, wird ihr Block ausgeführt — und der Rest der Kette übersprungen.


if (level < 5)
{
  write("Anfänger.\n");
}
else if (level < 20)
{
  write("Fortgeschrittener.\n");
}
else
{
  write("Veteran.\n");
}
      

Lies das so: „Wenn Level < 5: Anfänger. Sonst, wenn Level < 20: Fortgeschrittener. Sonst: Veteran.“ Dadurch sind die drei Fälle klar getrennt und decken zusammen alle Möglichkeiten ab.

Die Reihenfolge ist entscheidend

Die Bedingungen werden von oben nach unten geprüft. Sobald eine zutrifft, wird der Rest übersprungen. Das hat einen wichtigen Effekt: Wenn deine Bedingungen sich überlappen, gewinnt die obere:


// Falsch herum geordnet:
if (level >= 1)        write("Stufe 1+\n");        // matcht IMMER
else if (level >= 10)  write("Stufe 10+\n");       // wird nie erreicht
else if (level >= 50)  write("Stufe 50+\n");       // wird nie erreicht

// Richtig: spezifisch zuerst, allgemein zuletzt:
if      (level >= 50)  write("Stufe 50+\n");
else if (level >= 10)  write("Stufe 10+\n");
else if (level >= 1)   write("Stufe 1+\n");
      

Wann else if, wann switch?

Wenn du einen Wert mit vielen festen Möglichkeiten vergleichst (Strings, Zahlen, Bereiche), ist switch oft lesbarer. Wenn du verschiedene Bedingungen prüfst, die auf unterschiedlichen Variablen beruhen, ist else if die richtige Wahl.

6.6 Vergleichs- und logische Operatoren

Bedingungen bauen meistens auf Vergleichen und logischen Verknüpfungen auf. Hier ist die Liste der wichtigsten Operatoren — und ihre Bedeutung in verständlichen Worten.

Vergleichsoperatoren

  • ==gleich. „Ist x gleich 10?“
  • !=ungleich. „Ist x nicht 10?“
  • <kleiner als
  • >größer als
  • <=kleiner oder gleich
  • >=größer oder gleich

Klassische Anfängerfalle: = ist kein Vergleich, sondern eine Zuweisung! Vergleiche brauchen zwei Gleichheitszeichen.


// FALSCH: Zuweisung statt Vergleich
if (x = 10) { ... }     // setzt x auf 10 und prüft dann, ob 10 != 0 (also immer wahr!)

// RICHTIG: Vergleich mit ==
if (x == 10) { ... }
      

Logische Operatoren

Mit logischen Operatoren verbindest du mehrere Bedingungen:

  • &&UND. Beide Bedingungen müssen wahr sein.
  • ||ODER. Mindestens eine Bedingung muss wahr sein.
  • !NICHT. Macht aus wahr falsch und umgekehrt.

if (pl && pl->QueryProp(P_LEVEL) >= 10)
{
  write("Zugriff erlaubt.\n");
}
// "Wenn pl existiert UND sein Level mindestens 10 ist..."

if (race == "Elf" || race == "Mensch")
{
  write("Du gehörst zur erlaubten Rassengruppe.\n");
}
// "Wenn die Rasse Elf ODER Mensch ist..."

if (!present("schluessel", this_player()))
{
  write("Du hast keinen Schlüssel.\n");
}
// "Wenn KEIN Schlüssel vorhanden ist..."
      

Short-Circuit-Auswertung

&& und || haben einen sehr nützlichen Effekt: Sie werten von links nach rechts aus und brechen ab, sobald das Ergebnis feststeht. Bei A && B wird B gar nicht erst ausgewertet, wenn A bereits falsch ist. Das nutzt man bewusst aus, um sichere Aufrufe zu schreiben:


// Wenn pl == 0 ist, wird QueryProp(P_LEVEL) NICHT aufgerufen — kein Crash!
if (pl && pl->QueryProp(P_LEVEL) >= 10)
  ...
      

Reihenfolge: Vergleichsoperatoren binden enger als &&, das wiederum enger als ||. Bei Unsicherheit: setze Klammern.

6.7 Die while-Schleife

Eine while-Schleife wiederholt einen Codeblock, solange eine Bedingung wahr ist. Sie prüft die Bedingung am Anfang jedes Durchlaufs: Wenn die Bedingung dort schon falsch ist, wird der Block überhaupt nicht ausgeführt.


int i;

i = 0;
while (i < 5)
{
  write("i = " + i + "\n");
  i++;
}
      

Lies das so: „Solange i kleiner als 5 ist, gib den Wert aus und erhöhe i um 1.“ Die Ausgabe ist 0, 1, 2, 3, 4 — bei i = 5 ist die Bedingung falsch und die Schleife endet.

Wann ist while die richtige Wahl?

while eignet sich, wenn du nicht im Voraus weißt, wie oft die Schleife laufen wird. Beispiele:


// Solange ich noch HP habe, kämpfe weiter:
while (this_object()->QueryProp(P_HP) > 0) {
  // ...
}

// Solange ein bestimmter Spieler im Raum ist:
while (present("odin", environment(this_object()))) {
  // ...
  break;   // sicherheitshalber!
}
      

Achtung: Endlosschleifen!

Wenn die Bedingung nie falsch wird, entsteht eine Endlosschleife. Das ist im MUD doppelt gefährlich: Es blockiert nicht nur dein Objekt, sondern den ganzen Driver — alle Spieler hängen, bis das Tick-Limit zuschlägt und einen „too long evaluation“-Fehler wirft.


// FALSCH: i wird nie verändert — Endlosschleife!
int i = 0;
while (i < 5) {
  write(i + "\n");
  // i++ vergessen!
}
      

Faustregel: Wenn du while schreibst, frage dich sofort: „Wo ändere ich die Bedingung, damit sie irgendwann falsch wird?“

6.8 Die for-Schleife

Die for-Schleife ist kompakter als while und wird häufig für Zählvorgänge genutzt — also wenn du genau weißt (oder ausrechnen kannst), wie oft die Schleife laufen soll.


int i;

for (i = 0; i < 5; i++)
{
  write("i = " + i + "\n");
}
      

Aufbau der for-Schleife

Eine for-Schleife hat drei Teile, getrennt durch Strichpunkte:

  1. Initialisierung (i = 0) — wird einmal am Anfang ausgeführt.
  2. Bedingung (i < 5) — wird vor jedem Durchlauf geprüft.
  3. Änderung (i++) — wird nach jedem Durchlauf ausgeführt.

Das ist im Grunde nur eine kompaktere Schreibweise einer while-Schleife:


// for(...) entspricht:
int i = 0;          // Initialisierung
while (i < 5) {     // Bedingung
  write("i = " + i + "\n");
  i++;              // Änderung
}
      

Variable direkt deklarieren

Modern wird die Schleifenvariable oft direkt im for deklariert. Sie lebt dann nur innerhalb der Schleife — sauberer und weniger Namens-Konflikte:


for (int i = 0; i < 5; i++) {
  write("i = " + i + "\n");
}
// hier ist i nicht mehr bekannt
      

Klassische Verwendungsmuster


// Über ein Array iterieren:
for (int i = 0; i < sizeof(arr); i++) {
  write(arr[i] + "\n");
}

// Rückwärts zählen:
for (int i = 10; i > 0; i--) {
  write(i + "\n");
}

// In Zweier-Schritten:
for (int i = 0; i < 100; i += 2) {
  ...
}
      

Tipp: Für Schleifen über Arrays oder Mappings ist foreach oft eleganter und weniger fehleranfällig (siehe Abschnitt 6.11b).

6.9 Schleifen über Inventare

Sehr typisch im Midgard MUD: Schleifen über Inventare von Objekten. Ein Spieler hat Items, ein Container hat Inhalt, ein Raum hat NPCs und Gegenstände — überall braucht man die „Liste aller Objekte da drin“. Dafür gibt es all_inventory(), das ein Array zurückgibt.


object ob;
object *inv;

inv = all_inventory(this_player());

foreach (ob : inv)
{
  write(object_name(ob) + "\n");
}
      

Lies das so: „Hol mir das Inventar des aktuellen Spielers (ein Array von Objekten), und dann gib für jedes Objekt darin den Namen aus.“ foreach ist besonders lesbar und ideal für Anfänger — du musst keinen Index zählen, kein sizeof() abfragen, kein „Index out of range“ befürchten.

Typische Inventar-Aufgaben


// Wie viele Lebewesen sind im Raum?
int count = 0;
foreach(object ob : all_inventory(environment(this_player()))) {
  if (living(ob)) count++;
}
write("Lebewesen im Raum: " + count + "\n");

// Suche das schwerste Objekt im Inventar:
object schwerstes;
int    max_gewicht = 0;
foreach(object ob : all_inventory(this_player())) {
  int g = ob->QueryProp(P_WEIGHT);
  if (g > max_gewicht) {
    max_gewicht = g;
    schwerstes  = ob;
  }
}

// Drop alle Lichtquellen ab:
foreach(object ob : all_inventory(this_player())) {
  if (ob->QueryProp(P_LIGHT) > 0)
    ob->move(environment(this_player()), M_PUT);
}
      

Wenn du verschachtelte Container hast (Beutel im Inventar, Schachtel im Beutel), gibt es deep_inventory() — das liefert auch alle indirekten Objekte zurück. Hilfreich beim Suchen, aber teurer als all_inventory().

6.10 break und continue

Innerhalb von Schleifen gibt es zwei Spezial-Anweisungen, mit denen du den normalen Ablauf unterbrechen kannst: break verlässt die Schleife komplett, continue überspringt den Rest des aktuellen Durchlaufs und macht mit dem nächsten weiter.

break — Schleife sofort verlassen


while (1)             // Endlosschleife — aber mit kontrolliertem Ausstieg
{
  if (x > 10)
    break;            // sobald x > 10 ist, Schleife verlassen
  x++;
}
write("Fertig, x = " + x + "\n");
      

Klassischer Use-Case: Suche in einer Liste, sobald du gefunden hast, was du suchst, brich ab — kein Sinn, weiterzusuchen.


object schwert;
foreach(object ob : all_inventory(this_player())) {
  if (ob->id("schwert")) {
    schwert = ob;
    break;            // gefunden, raus aus der Schleife
  }
}

if (schwert) write("Gefunden: " + schwert->Name(WER) + "\n");
      

continue — diesen Durchlauf überspringen


for (i = 0; i < 10; i++)
{
  if (i == 5)
    continue;         // 5 überspringen, aber weitermachen

  write(i + "\n");
}
// Ausgabe: 0, 1, 2, 3, 4, 6, 7, 8, 9 (5 fehlt!)
      

continue ist nützlich, wenn du eine Bedingung hast, bei der du bestimmte Elemente überspringen willst — etwa beim Filtern:


foreach(object ob : all_inventory(this_player())) {
  if (!ob->QueryProp(P_WEIGHT)) continue;     // Gewichtslose Items überspringen
  if (ob->QueryProp(P_NODROP))  continue;     // Nicht-fallbare überspringen
  // alles andere fallen lassen:
  ob->move(environment(this_player()), M_PUT);
}
      

Beide Anweisungen wirken nur auf die innerste Schleife. Bei verschachtelten Schleifen brichst du nur die innere ab — die äußere läuft weiter.

6.11 Die switch-Anweisung

switch eignet sich für die Situation, wenn du einen Wert mit vielen festen Möglichkeiten vergleichen möchtest. Statt einer langen Kette von else if-Vergleichen schreibst du eine übersichtliche Liste von case-Labels — der Compiler erkennt das Muster und kann sogar intern Sprungtabellen bauen, die schneller sind als if/else.


switch (race)
{
  case "elf":
    write("Du bist ein Elf.\n");
    break;

  case "zwerg":
    write("Du bist ein Zwerg.\n");
    break;

  default:
    write("Unbekannte Rasse.\n");
}
      

Aufbau eines switch

  • switch (ausdruck) — der zu vergleichende Wert. Kann ein int oder ein string sein.
  • case wert: — eine konkrete Möglichkeit. Mehrere cases pro switch sind die Regel.
  • break; — beendet den case-Zweig. Ohne break läuft der Code in den nächsten Zweig weiter (Fall-through)!
  • default: — wird ausgeführt, wenn keiner der case-Werte zutrifft. Optional, aber empfohlen.

Häufige Fehlerquelle: break vergessen

Ohne break läuft der Code in den nächsten case-Block weiter — das ist Fall-through. In LPC ist das wie in C erlaubt, aber selten gewollt:


// FALSCH: break vergessen — beide Texte werden ausgegeben!
switch (race) {
  case "elf":
    write("Du bist ein Elf.\n");
    // break vergessen → flow-through!
  case "zwerg":
    write("Du bist ein Zwerg.\n");    // wird AUCH bei "elf" ausgeführt
    break;
}
      

Es gibt seltene Fälle, wo Fall-through gewollt ist — dann markiere ihn klar:


switch (kommando) {
  case "n":
  case "norden":
    return go_north();   // beide Aliase führen zur gleichen Aktion
  case "s":
  case "süden":
    return go_south();
}
      

Zusatz-Features im Midgard MUD

LPC-switch kann mehr als sein C-Vorbild — siehe Abschnitt 6.11a für Bereiche (case 0..49:) und String-Mehrfach-Cases. Das macht switch oft die elegantere Wahl gegenüber langen if/else-Ketten.

Wann switch, wann if/else?

  • switch wenn du einen einzigen Wert (Zahl oder String) gegen viele konkrete Möglichkeiten vergleichst.
  • if/else wenn die Bedingungen unterschiedliche Variablen oder komplexe Ausdrücke prüfen.

6.11a switch mit Bereichen und Strings

LPC-switch kann mehr als sein C-Vorbild. Du darfst Bereiche (case 1..5:) und Strings als Labels verwenden. Das ist im Midgard MUD extrem nützlich für Würfeltabellen oder Eingabe-Parsing.

Bereiche (Number-Ranges)


switch(random(100))
{
  case 0..49:
    write("Nichts passiert.\n");
    break;

  case 50..89:
    write("Du fühlst einen leichten Schwindel.\n");
    break;

  case 90..98:
    write("Eine warme Welle der Heilung durchströmt dich!\n");
    this_player()->buffer_hp(20, 5);   // val + rate, beide Pflicht
    break;

  case 99:
    write("KRITISCHER TREFFER! Volle Heilung!\n");
    this_player()->restore_hit_points(this_player()->QueryProp(P_MAX_HP));
    break;
}
      

Strings als Labels

Klassisches Beispiel: ein [ Bewegungs-Befehl ], der mehrere Schreibweisen für die Himmelsrichtung akzeptieren soll:


switch(verb)
{
  case "norden":
  case "n":
    return go_north();

  case "süden":
  case "s":
    return go_south();

  default:
    notify_fail("Wohin willst du gehen?\n");
    return 0;
}
      

Fall-Through ist erlaubt — und gefährlich


switch(level)
{
  case 1:
    write("Du bist sehr unerfahren.\n");
    // Fall-through (KEIN break!) - läuft in case 2..5 weiter
  case 2..5:
    write("Du solltest vorsichtig sein.\n");
    break;
  case 6..10:
    write("Du bist erfahren.\n");
    break;
}
      

Faustregel: Fall-Through ist nur OK, wenn er absichtlich ist. Markiere ihn dann mit einem Kommentar // fall through.

6.11b foreach in voller Pracht

Die foreach-Schleife ist im Midgard MUD die idiomatische Wahl. Sie kann mehr, als die meisten Anfänger wissen.

Variante 1: Über ein Array


foreach(object ob : all_inventory(this_player()))
  write(ob->name(WER) + "\n");
      

Variante 2: Über einen Integer (Zählschleife)


foreach(int i : 10)            // 0..9
  write("i = " + i + "\n");
      

Variante 3: Über einen Bereich


foreach(int i : 1..6)          // 1..6 (inklusive)
  write("Würfel: " + i + "\n");
      

Variante 4: Über ein Mapping


mapping stats = ([ "str":15, "dex":12, "int":18 ]);

// nur Schlüssel:
foreach(string key : stats)
  write(key + "\n");

// Schlüssel + Wert (Width 1):
foreach(string key, int wert : stats)
  write(key + " = " + wert + "\n");
      

Variante 5: Mit Referenz (verändert das Original!)


int *werte = ({ 1, 2, 3, 4 });
foreach(int v : &werte)
  v *= 10;
write(sprintf("%O\n", werte));   // ({ 10, 20, 30, 40 })
      

Variante 6: Über String-Zeichen


foreach(int c : "Hallo")
  write(c + " ");                // 72 97 108 108 111
      

Variante 7: break und continue


foreach(object ob : all_inventory(this_player()))
{
  if (!ob->QueryProp(P_WEIGHT)) continue;    // überspringen
  if (ob->QueryProp(P_WEIGHT) > 5000) break; // abbrechen
  // ...
}
      

Lies das alles in /doc/LPC/foreach.

6.11c Der Conditional-Operator ?:

Der ternäre Operator ist eine kompakte Form von if/else:


int hp = this_player()->QueryProp(P_HP);
string state = (hp > 50) ? "gesund" : "verletzt";

// Statt:
string state2;
if (hp > 50) state2 = "gesund";
else         state2 = "verletzt";
      

Im Midgard MUD oft für Pluralformen oder Geschlechter:


write("Du siehst " + (count == 1 ? "einen" : count + "")
    + " Drachen.\n");

string er_sie = (gender == MALE) ? "er" : "sie";
      

Faustregel: Wenn das Ganze in eine Zeile passt und gut lesbar ist, ist ?: ok. Verschachtelte ternäre Operatoren werden schnell unleserlich — dann lieber if/else.

6.11d Short-Circuit: && und || mit Tricks

&& und || werten von links nach rechts aus und brechen ab, sobald das Ergebnis feststeht. Das ist nicht nur effizienter — es ist auch ein häufig genutztes Pattern im Midgard MUD.

Sichere Objekt-Tests


// Wenn pl 0 ist, wird QueryProp(P_LEVEL) nie aufgerufen — kein Crash.
if (pl && pl->QueryProp(P_LEVEL) >= 10)
  write("Zugriff erlaubt.\n");
      

Default-Werte mit ||


string name = pl->query_real_name() || "Unbekannt";
// Wenn query_real_name() 0/leer liefert, wird "Unbekannt" genommen.
      

Bedingte Aktion mit &&


// Aktion nur ausführen, wenn Bedingung wahr ist:
living(ob) && ob->Defend(10, ({DT_BLUDGEON}), 0, this_object());
// Lesbarer als:
// if (living(ob)) ob->Defend(...);
// (Aber bitte nur, wenn es kurz bleibt!)
      

6.11e LPC-Operatoren auf Arrays, Strings, Mappings

Was viele Anfänger nicht wissen: +, -, &, | funktionieren auch auf Sammlungen.


// Arrays
({ 1,2,3 }) + ({ 4,5 })       // → ({ 1,2,3,4,5 })
({ 1,2,3,2,1 }) - ({ 2 })     // → ({ 1,3,1 })   alle 2en raus
({ 1,2,3 }) & ({ 2,3,4 })     // → ({ 2,3 })    Schnittmenge
({ 1,2,3 }) | ({ 3,4,5 })     // → ({ 1,2,3,4,5 }) Vereinigung

// Strings
"Hallo " + "Welt"             // "Hallo Welt"
"Hallo Welt" - "l"            // "Hao Wet"  (alle l weg)
"abc" * 3                     // "abcabcabc"

// Mappings (gleiche Width):
([ "a":1 ]) + ([ "b":2 ])     // ([ "a":1, "b":2 ])
([ "a":1, "b":2 ]) - ([ "a" ])  // ([ "b":2 ])
      

Diese Operatoren sind sehr schnell, weil sie Driver-intern implementiert sind. Nutze sie statt manueller Schleifen, wann immer es passt.

6.11f Range-Indexing und Slices

Auf Arrays und Strings kannst du mit [from..to] ganze Bereiche herausschneiden. Mit <n zählst du vom Ende.


int *a = ({ 0, 1, 2, 3, 4, 5, 6, 7 });

a[2..4]    // ({ 2, 3, 4 })
a[3..]     // ({ 3, 4, 5, 6, 7 })  - "ab Index 3 bis Ende"
a[..2]     // ({ 0, 1, 2 })        - "Anfang bis Index 2"
a[<3..]    // ({ 5, 6, 7 })        - die letzten 3
a[<3..<2]  // ({ 5, 6 })           - <n> heißt "n von hinten"
      

Slice-Assignment (sehr mächtig)


int *a = ({ 0, 1, 2, 3, 4 });
a[2..3] = ({ 99 });           // a → ({ 0, 1, 99, 4 })  (Größe änderbar!)
a[1..0] = ({ "x", "y" });     // einfügen vor Position 1
      

Slice-Assignment kann Arrays vergrößern und verkleinern. Das ist im Midgard oft eleganter als loop-basiertes Umkopieren — aber bitte nicht in heißen Schleifen.

6.11g catch() und Fehler abfangen

catch() ist keine Schleife, gehört aber thematisch hierher: Es ist der einzige Weg, Laufzeitfehler abzufangen, ohne dass dein Code stirbt.


mixed err = catch(ob->risky_call());
if (err)
{
  // err ist ein Array (im modernen LDMud) mit Fehlerdetails
  log_file("error", sprintf("risky_call: %O\n", err));
}
      

Spezialformen (alle als Symbole im 2. Argument):

  • catch(expr; 'nolog) — Fehler nicht ins Log
  • catch(expr; 'publish) — runtime_error() trotzdem rufen
  • catch(expr; 'reserve, n) — eigenes Tick-Reserve setzen

Faustregel: catch() nur dort, wo du weißt, dass etwas schiefgehen kann (fremder Code, riskante Konvertierung). Nie als Universal-Wrapper — das versteckt echte Bugs.

6.12 Typische Anfängerfehler

  • Endlosschleifen
  • = statt ==
  • break im switch vergessen
  • Zu verschachtelte if-Blöcke
  • foreach mit Referenz, ohne sie zu wollen (&arr)
  • Slice-Assignment in heißen Schleifen — verbraucht Tonnen Speicher
  • catch() als Universalwrapper, der echte Fehler verschluckt

6.13 Mini-Übungen

  1. Schreibe eine Schleife, die von 1 bis 10 zählt.
  2. Prüfe, ob ein Spieler ein bestimmtes Level hat.
  3. Gib alle Gegenstände im Inventar aus.

6.14 Zusammenfassung

  • if steuert Entscheidungen
  • Schleifen wiederholen Code
  • break und continue steuern Schleifen
  • switch vereinfacht viele Fälle

Kontrollstrukturen geben dir Kontrolle über dein Programm. Ohne sie wäre LPC praktisch nutzlos.

Ausblick

In Kapitel 7 lernst du Arrays und Mappings – komplexe Datenstrukturen, die in LPC ständig benutzt werden.

Weiter zu Kapitel 7