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
- Erstelle ein Array mit fünf Zahlen und gib sie aus.
- Erstelle ein Mapping für Spielerattribute.
- 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