MIDGARDAudhumbla 0.7

Kapitel 7: Arrays & Mappings

In diesem Kapitel lernst du zwei der wichtigsten Datentypen in LPC kennen: Arrays und Mappings. Sie erlauben es dir, mehrere Werte strukturiert zu speichern und zu verarbeiten. Ohne Arrays und Mappings wäre modernes LPC-Programmieren im Midgard MUD praktisch unmöglich.

Weiter zu Kapitel 8

Danach: Objekte, Vererbung und das MUD-Objektmodell.

7.1 Warum brauchen wir Arrays und Mappings?

Bisher hast du hauptsächlich mit einzelnen Variablen gearbeitet: eine Zahl, ein String, ein Objekt. Das funktioniert wunderbar für Werte, die genau einmal vorkommen — Lebenspunkte, Namen, der aktuelle Raum. Aber sobald du mit Mengen arbeitest, reicht das nicht mehr aus. In echten Programmen — und besonders in einem MUD — sind Mengen die Regel:

Typische Fragen, die mit einzelnen Variablen schwer beantwortbar sind:

  • Wie speichere ich mehrere Gegenstände im Inventar eines Spielers?
  • Wie merke ich mir, welche Spieler gerade online sind?
  • Wie ordne ich Namen bestimmten Werten zu (z.B. "str" → 15)?
  • Wie speichere ich eine Liste von Räumen, durch die ein Wikinger patrouilliert?
  • Wie verwalte ich Quest-Status für Hunderte verschiedener Quests?

Stell dir vor, du müsstest Variablen wie item1, item2, item3, … bis item500 anlegen. Das wäre nicht nur unpraktisch, sondern macht es unmöglich, mit „beliebig vielen“ Items umzugehen. Genau dafür gibt es zwei Spezialdatentypen:

  • Arrays — wenn die Reihenfolge wichtig ist und du nummerierten Zugriff brauchst (Element 1, Element 2, …)
  • Mappings — wenn du einem Schlüssel (z.B. einem Namen) einen Wert zuordnen möchtest, ohne Reihenfolge

Beide Datentypen sind im Midgard MUD allgegenwärtig. Wenn du sie sicher beherrschst, wirst du fast jeden Code in der Mudlib lesen und schreiben können.

7.2 Arrays – geordnete Listen von Werten

Ein Array (oft auch „Liste“ oder „Vektor“ genannt) ist eine geordnete Sammlung von Elementen. Drei Eigenschaften zeichnen Arrays aus:

  • Geordnet: Es gibt ein erstes Element, ein zweites, ein drittes. Die Reihenfolge ist wichtig und bleibt erhalten.
  • Indiziert: Jedes Element hat eine Position (Index) — und Indizes beginnen in LPC immer bei 0, nicht bei 1.
  • Gleichberechtigt: Alle Elemente sind vom gleichen Typ (in einem typisierten Array). Du kannst aber auch mixed * machen, dann ist alles erlaubt.

int *numbers;

numbers = ({ 10, 20, 30 });
// Drei Zahlen, in dieser Reihenfolge gespeichert.
      

Die Notation ist hier zentral: Arrays werden mit ({ ... }) geschrieben — also runde Klammern, jeweils mit einer geschweiften Klammer dahinter bzw. davor. Im Variablentyp markierst du Arrays mit einem * nach dem Typ: int*, string*, object*, mixed*.

Dieses Array enthält drei Zahlen:

  • numbers[0] → 10 (das erste Element)
  • numbers[1] → 20 (das zweite)
  • numbers[2] → 30 (das dritte)

Wenn du dich an die Indizierung ab 0 erinnerst, ist alles andere geradlinig: bei einem Array mit n Elementen sind die gültigen Indizes 0 bis n-1.

Leeres Array und einzelnes Element


int *leer = ({ });        // leeres Array, Größe 0
int *eins = ({ 42 });     // ein einzelnes Element

// Achtung: ({ }) ist NICHT dasselbe wie 0!
// 0 = "noch nichts", ({ }) = "ein leeres Array".
      

Auch komplizierte Inhalte sind erlaubt


string  *namen   = ({ "Thor", "Odin", "Freya" });
object  *spieler = users();        // alle Spieler online
mixed   *daten   = ({ 1, "zwei", this_player(), ({ 4, 5 }) });
// Auch verschachtelte Arrays gehen — das letzte Element ist
// ein Array mit zwei Zahlen drin.
      

7.3 Arrays auslesen

Um auf ein einzelnes Element eines Arrays zuzugreifen, schreibst du den Variablennamen und in eckigen Klammern den Index dahinter — genau wie das Anschreiben eines Hauses mit der Nummer auf der Straße:


int *numbers = ({ 10, 20, 30 });

write(numbers[0] + "\n");  // gibt 10 aus
write(numbers[1] + "\n");  // gibt 20 aus
write(numbers[2] + "\n");  // gibt 30 aus
      

Vom Ende her zählen

Mit <n zählst du vom Ende statt vom Anfang. Das ist ein LPC-Spezialfeature und im MUD-Code sehr beliebt:


int *a = ({ 'a', 'b', 'c', 'd', 'e' });

a[<1]   // 'e' — das letzte Element
a[<2]   // 'd' — das vorletzte
a[0]    // 'a' — das erste (wie gewohnt)
      

Niemals außerhalb der Grenzen!

Wenn du auf einen Index zugreifst, der nicht existiert, gibt der Driver einen Runtime-Error: „Index out of range“. Dein Code stoppt mitten in der Aktion — das willst du nicht.


int *a = ({ 10, 20, 30 });

write(a[5]);     // FEHLER! Index 5 existiert nicht (nur 0..2)
write(a[-1]);    // FEHLER! Negative Indizes sind keine "vom Ende"-Notation
                 // — dafür brauchst du a[<1]
      

Sicher zugreifen

Wenn du nicht sicher bist, ob ein Index existiert, prüfe vorher mit sizeof():


if (i < sizeof(a))
  write(a[i] + "\n");

// Oder noch besser: über das ganze Array iterieren mit foreach
// (siehe Abschnitt 7.5 und 7.11).
      

7.4 Größe eines Arrays

Mit der Funktion sizeof() erhältst du die Anzahl der Elemente in einem Array. Diese Funktion ist eine der meistgenutzten in LPC, weil du sie ständig brauchst — bei Schleifen, bei Existenz-Tests, bei Prüfungen vor dem Indexzugriff.


int *numbers = ({ 10, 20, 30 });
int size;

size = sizeof(numbers);  // 3
      

Typische Verwendungsmuster


// Ist das Array leer?
if (sizeof(arr) == 0) write("Leer.\n");

// Oder kompakter (in LPC ist 0 == false):
if (!sizeof(arr)) write("Leer.\n");

// Hat das Array überhaupt Elemente?
if (sizeof(arr)) write("Mindestens eins drin.\n");

// Klassische for-Schleife
for (int i = 0; i < sizeof(arr); i++)
  write(arr[i] + "\n");

// Letztes Element (gibt's eines?)
if (sizeof(arr))
  write("Letztes: " + arr[<1] + "\n");
      

Performance-Tipp

In sehr engen Schleifen kann es sich lohnen, sizeof(arr) einmal in eine lokale Variable zu schreiben statt es bei jedem Durchlauf neu zu berechnen:


// Weniger effizient (sizeof wird N-mal berechnet):
for (int i = 0; i < sizeof(arr); i++) { ... }

// Schneller — sizeof nur einmal:
int len = sizeof(arr);
for (int i = 0; i < len; i++) { ... }

// Noch besser: foreach (kein sizeof nötig)
foreach(int x : arr) { ... }
      

sizeof() funktioniert übrigens auch auf Strings (Anzahl der Zeichen) und auf Mappings (Anzahl der Schlüssel). Sehr praktisch — du musst nicht für jeden Datentyp eine andere Funktion lernen.

7.5 Über Arrays iterieren

Meist willst du alle Elemente eines Arrays verarbeiten.


int i;

for (i = 0; i < sizeof(numbers); i++)
{
  write(numbers[i] + "\n");
}
      

Alternativ (und moderner):


int n;

foreach (n : numbers)
{
  write(n + "\n");
}
      

7.6 Arrays verändern

Arrays sind in LPC dynamisch — du kannst sie zur Laufzeit verändern, vergrößern, verkleinern, neu zusammensetzen. Das ist anders als in C, wo Arrays eine feste Größe haben. In LPC kannst du sie wie Mengen oder Listen behandeln.

Hinzufügen mit + oder +=


int *numbers = ({ 10, 20, 30 });

numbers += ({ 40 });          // → ({ 10, 20, 30, 40 })
numbers += ({ 50, 60 });      // → ({ 10, 20, 30, 40, 50, 60 })
numbers = numbers + ({ 70 }); // gleichbedeutend mit +=
      

Beachte: Du musst dem Array immer ein Array hinzufügen, nicht einen einzelnen Wert. numbers += 40 würde nicht funktionieren — du brauchst numbers += ({ 40 }). Diese Klammer-Notation ist anfangs ungewohnt, wird aber schnell Reflex.

Entfernen mit - oder -=


int *numbers = ({ 10, 20, 30, 20, 40 });

numbers -= ({ 20 });          // entfernt ALLE 20er
// → ({ 10, 30, 40 })
      

Das Minus entfernt alle Vorkommen der gegebenen Werte. Das ist oft praktisch, gelegentlich überraschend — wenn du nur das erste Vorkommen entfernen willst, brauchst du eine andere Methode (z.B. mit member() und Index-Slicing).

Vereinigung und Schnittmenge


int *a = ({ 1, 2, 3 });
int *b = ({ 3, 4, 5 });

a | b      // ({ 1, 2, 3, 4, 5 })   Vereinigung (wie eine Menge)
a & b      // ({ 3 })               Schnittmenge
      

Diese Operatoren sind im Driver implementiert und sehr schnell. Nutze sie statt manueller Schleifen, wann immer du Mengenlogik brauchst.

Einzelnes Element ändern


numbers[0] = 99;     // Erstes Element auf 99 setzen

// Slice-Assignment (siehe Kapitel 6.11f):
numbers[1..2] = ({ 11, 22 });
      

7.7 Typische Array-Typen im Midgard MUD

Der Stern * nach dem Typ macht aus einer einzelnen Variable ein Array. Die häufigsten Array-Typen, denen du im Midgard MUD begegnest:


string *names;        // Liste von Namen, IDs, Adjektiven
object *inv;          // Liste von Objekten (z.B. ein Inventar)
int    *werte;        // Liste von Zahlen (z.B. Schadenswerte)
mixed  *data;         // Bunt gemischtes Array
      

Was du im echten Mudlib-Code oft siehst

  • IDs eines Items: AddId(({ "schwert", "altes schwert", "klinge" })) — ein Array von Strings, mit denen das Objekt ansprechbar ist.
  • Inventar eines Spielers: object *inv = all_inventory(pl); — ein Array von Objekten. Spielerseite siehe [ Hilfe: Inventur ].
  • Schadenstypen einer Waffe: SetProp(P_DAM_TYPE, ({ DT_BLUDGEON, DT_FIRE })).
  • Liste von Räumen für eine Patrouille: string *route = ({ "/d/x/raum1", "/d/x/raum2" }).
  • NPC-Chats: SetProp(P_CHATS, ({ "Hrafn brummt.", "Hrafn schaut sich um." })).

Wann mixed *?

mixed *-Arrays können alles enthalten — sind aber schwieriger zu debuggen, weil der Compiler dir nicht hilft, wenn du Element 3 als String erwartest, aber tatsächlich ein Objekt drinsteht. Nutze mixed * nur, wenn die Datenstruktur das wirklich erfordert (z.B. „Element 0 ist Spieler, Element 1 ist Schadenshöhe als int, Element 2 ist Schadenstyp als String“).

7.8 Mappings – Schlüssel-Wert-Paare

Ein Mapping ist die zweite große Sammeldatenstruktur neben Arrays. Während Arrays Werte über Indizes ansprechen (Position 0, 1, 2…), nutzen Mappings Schlüssel. Das ist vergleichbar mit einem Wörterbuch: zu jedem Eintrag gibt es einen Begriff (Schlüssel) und eine Erklärung (Wert).

Im MUD sind Mappings ideal für alles, wo Zuordnungen wichtig sind und die Reihenfolge keine Rolle spielt: Spielerattribute, Konfigurationen, Suchindizes, Quest-Status, Detail-Beschreibungen eines Raums.


mapping stats;

stats = ([
  "str" : 10,    // Stärke
  "dex" : 12,    // Geschick
  "int" : 15,    // Intelligenz
  "con" : 11,    // Konstitution
]);
      

Die Notation: Mappings stehen in Klammern ([ ... ]) (mit eckigen Klammern, anders als Arrays mit runden: ({ ... })). Schlüssel und Wert werden mit einem Doppelpunkt getrennt, einzelne Einträge mit Komma. Hier ordnest du Namen konkrete Werte zu — du kannst dir den ganzen Eintrag wie eine Karteikarte mit „Vorder- und Rückseite“ vorstellen.

Was kann ein Schlüssel sein?

Schlüssel können verschiedenste Typen haben — am häufigsten sind Strings, aber auch Zahlen oder Objekte sind möglich:


mapping per_string = ([ "str":10, "int":15 ]);    // String-Schlüssel
mapping per_int    = ([ 1:"a", 2:"b", 3:"c" ]);   // Int-Schlüssel
mapping per_object = ([ this_player():time() ]);  // Objekte als Schlüssel (selten)
      

Was kann ein Wert sein?

Werte können beliebige Typen sein — auch Mappings, Arrays, Objekte:


mapping items = ([
  "schwert"  : ({ DT_BLUDGEON, 50 }),       // Array als Wert
  "config"   : ([ "color":"red", "size":3 ]),// Mapping als Wert
  "owner"    : this_player(),                // Objekt als Wert
]);
      

7.9 Werte aus Mappings lesen

Werte aus einem Mapping liest du mit eckigen Klammern [] — wie bei Arrays. Aber statt einer Zahl gibst du den Schlüssel an:


int strength;

strength = stats["str"];     // 10
write(stats["int"] + "\n");  // 15
      

Nicht-existierender Schlüssel

Anders als bei Arrays gibt es keinen Fehler, wenn ein Schlüssel nicht existiert. Stattdessen bekommst du den Default-Wert 0 zurück (bzw. 0, was für jeden Typ als „leer“ funktioniert):


write(stats["foo"]); // 0  (existiert nicht, kein Fehler)
      

Das ist meistens praktisch, kann aber zu subtilen Bugs führen — wenn ein gültiger Wert auch 0 wäre, kannst du ihn nicht von „nicht vorhanden“ unterscheiden. In dem Fall nutze member():


mapping zaehl = ([ "appel": 0, "birne": 5 ]);

if (zaehl["appel"])     // 0 — als ob "appel" gar nicht existiert!
  write("Es gibt Äpfel.\n");

if (member(zaehl, "appel"))    // 1 — Schlüssel ist da
  write("Es gibt einen Eintrag für Äpfel.\n");
      

7.10 Werte in Mappings setzen und ändern

Werte schreiben funktioniert mit demselben Klammeroperator wie das Lesen — nur eben auf der linken Seite einer Zuweisung. Der Vorteil: Wenn der Schlüssel noch nicht existiert, wird er automatisch angelegt. Mappings wachsen automatisch.


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

stats["str"] = 20;    // bestehender Schlüssel — Wert ändern
stats["wis"] = 8;     // neuer Schlüssel — wird hinzugefügt

// stats ist jetzt: ([ "str":20, "dex":12, "int":15, "wis":8 ])
      

Eintrag löschen mit m_delete()


m_delete(stats, "wis");   // entfernt "wis" aus dem Mapping
// stats ist jetzt: ([ "str":20, "dex":12, "int":15 ])
      

Mehrere Einträge auf einmal


// Mappings vereinen mit + oder +=:
mapping a = ([ "x":1, "y":2 ]);
mapping b = ([ "y":3, "z":4 ]);
a += b;     // ([ "x":1, "y":3, "z":4 ])
            // gleiche Schlüssel: rechts überschreibt links

// Schlüsselmenge subtrahieren:
a -= ([ "x" ]);   // entfernt Eintrag mit Schlüssel "x"
      

Mappings sind im Midgard MUD eines der Standardwerkzeuge. Praktisch jedes Standardobjekt hat eingebaute Mappings: Property-Mapping, Detail-Mapping, Befehl-Mapping, Item-Mapping. Wenn du sie sicher beherrschst, hast du zwei wichtige Hürden gleichzeitig genommen.

7.11 Über Mappings iterieren

Du kannst Schlüssel und Werte getrennt auslesen.


string key;

foreach (key : m_indices(stats))
{
  write(key + ": " + stats[key] + "\n");
}
      

Alternativ:


string k;
int v;

foreach (k, v : stats)
{
  write(k + ": " + v + "\n");
}
      

7.11a Mappings mit mehreren Werten pro Schlüssel (Width)

LDMud-Mappings können pro Schlüssel mehrere Werte halten – das nennt man Width (Breite). Im Midgard ist das z.B. für Detailbeschreibungen üblich, die je nach Sinn (sehen, riechen, hören) andere Texte liefern.


// Mapping mit Width 3:
mapping m = ([
  "stein" : "Ein Stein."; "Er riecht erdig."; "Stille." ,
  "fluss" : "Ein Fluss."; "Es riecht modrig."; "Es plaetschert leise.",
]);

// Zugriff per zweitem Index 0..2:
write(m["stein", 0]);   // "Ein Stein."
write(m["fluss", 2]);   // "Es plaetschert leise."

int w = widthof(m);     // 3
      

Mapping ohne Werte – ein „Set“

Ein Mapping kann auch null Werte pro Schlüssel haben. Dann nutzt man es wie eine Menge (Set): nur die Schlüssel zählen. Das ist für schnelle Mitgliedstests deutlich besser als member() auf Arrays.


mapping seen = m_allocate(0, 0);   // width = 0
seen["odin"] = 0;                   // Schlüssel ist da
seen["thor"] = 0;

if (member(seen, "odin"))
  write("Odin war schon hier.\n");
      

7.11b Wichtige Mapping-Helfer

  • m_indices(map) – alle Schlüssel als Array
  • m_values(map [, n]) – alle Werte (n-te Spalte)
  • m_delete(map, key) – Schlüssel entfernen
  • m_contains(&v, map, key) – sicheres Lesen mit Existenztest
  • walk_mapping(map, fun) – über alle Einträge iterieren
  • filter(map, fun) / map_mapping(map, fun) – transformieren
  • copy(map) – explizite Kopie (Mappings sind Referenzen!)

Achtung Referenz-Falle – Mappings (und Arrays) werden by reference übergeben:


mapping a = ([ "x": 1 ]);
mapping b = a;            // SELBES Mapping!
b["x"] = 99;
write(a["x"]);            // -> 99 (a wurde mitverändert)

mapping c = copy(a);
c["x"] = 0;
write(a["x"]);            // -> 99 (a unverändert)
      

Wenn QueryProp() ein Mapping zurückliefert: Veränderst du es, veränderst du oft die Property im Originalobjekt mit! Lieber kopieren oder über SetProp gehen.

7.12 Arrays vs. Mappings – wann was?

  • Array → Reihenfolge wichtig
  • Array → Zugriff über Zahlen
  • Mapping → Zuordnung wichtig
  • Mapping → Zugriff über Namen

Beispiel:


// Inventar → Array
object *inv = all_inventory(this_player());

// Attribute → Mapping
mapping attrs = this_player()->QueryProp(P_ATTRIBUTES);
      

7.13 Häufige Anfängerfehler

  • Array-Grenzen überschreiten
  • sizeof() vergessen
  • Mapping-Schlüssel verwechseln
  • mixed zu oft verwenden

7.14 Mini-Übungen

  1. Erstelle ein Array mit fünf Zahlen und gib sie aus.
  2. Erstelle ein Mapping für Spielerattribute.
  3. Iteriere über ein Mapping und gib alle Paare aus.

7.15 Zusammenfassung

  • Arrays speichern geordnete Daten
  • Mappings speichern Zuordnungen
  • foreach vereinfacht Iterationen
  • Beide Datentypen sind essenziell

Mit Arrays und Mappings kannst du nun echte Datenstrukturen bauen.

Ausblick

In Kapitel 8 geht es um Objekte, Vererbung und das Objektmodell des Midgard MUDs – das Herzstück von LPC.

Weiter zu Kapitel 8