Zum Inhalt springen

cat posts/python-lernen-04-sammlungen.md

Python lernen (Teil 4): Sammlungen

Listen, Tupel, Dictionaries und Sets – damit beschreiben wir die Dungeon-Welt als Daten und geben unserem Abenteurer ein richtiges Inventar.

In Teil 3 haben wir einen Game-Loop gebaut, der auf die Befehle des Spielers reagiert. Die möglichen Wege und Ereignisse steckten dabei direkt in einer langen if-elif-Kette.

Bei zwei oder drei Räumen funktioniert das noch. Besteht unser Dungeon später aber aus dreißig Räumen, wird der Game-Loop schnell unübersichtlich.

Heute lernen wir deshalb Sammlungen kennen. Mit ihnen können wir mehrere Werte gemeinsam speichern und die Welt unseres Dungeons als Daten beschreiben.

Python bringt dafür verschiedene Typen von Sammlungen (Collections) mit:

  • Listen
  • Tupel
  • Dictionaries
  • Sets

Sie erfüllen unterschiedliche Aufgaben. Am Ende dieses Teils verwenden wir sie, um Räume, Ausgänge, Gegenstände und den Player State übersichtlich abzubilden.

Listen: geordnet und veränderbar

Eine Liste speichert mehrere Werte in einer bestimmten Reihenfolge. Listen werden mit eckigen Klammern geschrieben:

inventar = ["Fackel", "Brot", "Seil"]

Die einzelnen Werte heißen Elemente. Eine Liste kann beliebig viele Elemente enthalten oder auch leer sein:

leeres_inventar = []

Auf Elemente zugreifen

Jedes Element hat eine Position, den sogenannten Index. Die Zählung beginnt bei 0:

inventar = ["Fackel", "Brot", "Seil"]

print(inventar[0])
print(inventar[1])
print(inventar[2])

Die Ausgabe lautet:

Fackel
Brot
Seil

Die Indizes dieser Liste sehen damit so aus:

Index Element
0 "Fackel"
1 "Brot"
2 "Seil"

Dass die Zählung bei 0 beginnt, ist anfangs ungewohnt, in der Programmierung aber sehr verbreitet.

Mit negativen Indizes kannst Du vom Ende der Liste aus zugreifen:

print(inventar[-1])

Die Ausgabe lautet:

Seil

-1 bezeichnet das letzte Element, -2 das vorletzte und so weiter.

Listen verändern

Listen sind veränderbar (mutable). Mit append(...) fügst Du ein einzelnes Element am Ende hinzu:

inventar = ["Fackel", "Brot"]
inventar.append("Schlüssel")

print(inventar)

Die Ausgabe lautet:

['Fackel', 'Brot', 'Schlüssel']

Mit remove(...) entfernst Du das erste Element mit einem bestimmten Wert:

inventar.remove("Brot")

print(inventar)

Danach enthält die Liste nur noch:

['Fackel', 'Schlüssel']

Möchtest Du eine Liste um mehrere Elemente erweitern, kannst Du extend(...) verwenden:

inventar = ["Fackel"]
fund = ["Schlüssel", "Heiltrank"]

inventar.extend(fund)

print(inventar)

Die Ausgabe lautet:

['Fackel', 'Schlüssel', 'Heiltrank']

Der Unterschied zu append(...) ist wichtig. Folgendes:

inventar.append(fund)

fügt die komplette Liste fund als ein einziges Element hinzu:

['Fackel', ['Schlüssel', 'Heiltrank']]

Wir hätten dann eine Liste innerhalb unserer Liste.

Anzahl der Elemente bestimmen

Mit len(...) ermittelst Du die Länge einer Liste:

inventar = ["Fackel", "Brot", "Seil"]

print(len(inventar))

Die Ausgabe lautet:

3

Eine leere Liste besitzt entsprechend die Länge 0.

Prüfen, ob ein Element enthalten ist

Mit in kannst Du prüfen, ob ein bestimmter Wert in einer Liste vorkommt:

inventar = ["Fackel", "Brot", "Seil"]

print("Brot" in inventar)
print("Schlüssel" in inventar)

Die Ausgabe lautet:

True
False

Das Ergebnis lässt sich direkt in einer Bedingung verwenden:

if "Schlüssel" in inventar:
    print("Du kannst die Tür öffnen.")

Eine Liste durchlaufen

Mit einer for-Schleife kannst Du jedes Element der Reihe nach verarbeiten:

inventar = ["Fackel", "Brot", "Seil"]

for gegenstand in inventar:
    print(f"- {gegenstand}")

Die Ausgabe lautet:

- Fackel
- Brot
- Seil

Einen Index brauchst Du dafür nicht. Bei jedem Durchlauf verweist gegenstand auf das nächste Element der Liste.

Tupel: geordnet und unveränderbar

Ein Tupel ähnelt einer Liste: Es ist geordnet und kann mehrere Werte enthalten. Es ist jedoch unveränderbar (immutable): Die Elemente eines Tupels lassen sich nach seiner Erstellung nicht mehr ersetzen, hinzufügen oder entfernen.

Tupel werden üblicherweise mit runden Klammern geschrieben:

position = (3, 7)

Auf die Elemente greifst Du wie bei einer Liste über ihren Index zu:

print(position[0])
print(position[1])

Die Ausgabe lautet:

3
7

Eine Position aus einer X- und einer Y-Koordinate ist ein typischer Anwendungsfall: Beide Werte gehören zusammen und werden als Einheit behandelt.

Du kannst sie auch direkt auf zwei Variablen verteilen:

position = (3, 7)
x, y = position

print(f"X: {x}, Y: {y}")

Das nennt sich Unpacking.

Tupel lassen sich nicht verändern

Der folgende Versuch führt zu einem TypeError:

position = (3, 7)
position[0] = 9

Python erlaubt es nicht, einem Element des Tupels nachträglich einen anderen Wert zuzuweisen.

Tupel eignen sich deshalb gut für zusammengehörige Werte, deren Zusammensetzung nach dem Erstellen gleich bleiben soll.

Ein Tupel mit nur einem Element

Bei einem Tupel mit nur einem Element ist das Komma entscheidend:

ein_element = ("Schlüssel",)

Ohne das Komma entsteht lediglich ein String in Klammern:

kein_tupel = ("Schlüssel")

print(type(kein_tupel))

Die Ausgabe lautet:

<class 'str'>

Technisch ist es also das Komma, das das Tupel erzeugt, nicht die runden Klammern allein.

Dictionaries: Werte über Schlüssel erreichen

Ein Dictionary, kurz dict, ordnet Schlüsseln bestimmte Werte zu.

Während Du bei einer Liste über einen numerischen Index auf ein Element zugreifst, verwendest Du bei einem Dictionary einen Schlüssel:

raum = {
    "name": "Halle",
    "beschreibung": "Eine dunkle Halle mit moderigem Geruch.",
    "besucht": False,
}

Ein Dictionary wird mit geschweiften Klammern geschrieben. Jeder Eintrag besteht aus einem Schlüssel und dem zugehörigen Wert:

"beschreibung": "Eine dunkle Halle mit moderigem Geruch."

Der Doppelpunkt trennt Schlüssel und Wert voneinander.

Dictionaries behalten die Reihenfolge bei, in der ihre Einträge eingefügt wurden. Anders als bei einer Liste greifst Du aber nicht über diese Position auf einen Wert zu, sondern über seinen Schlüssel.

Auf einen Wert zugreifen

Den Wert eines bestimmten Schlüssels erhältst Du mit eckigen Klammern:

print(raum["name"])
print(raum["beschreibung"])

Die Ausgabe lautet:

Halle
Eine dunkle Halle mit moderigem Geruch.

Werte verändern und ergänzen

Dictionaries sind wie Listen mutable. Du kannst einen vorhandenen Wert verändern:

raum["besucht"] = True

Mit derselben Schreibweise lässt sich ein neuer Eintrag ergänzen:

raum["licht"] = "dunkel"

Existiert der Schlüssel bereits, wird sein Wert ersetzt. Existiert er noch nicht, wird ein neuer Eintrag angelegt.

Prüfen, ob ein Schlüssel existiert

Der Operator in prüft bei einem Dictionary dessen Schlüssel:

print("beschreibung" in raum)
print("monster" in raum)

Die Ausgabe lautet:

True
False

Das ist praktisch, wenn ein Eintrag nur in manchen Dictionaries vorhanden ist:

if "monster" in raum:
    print("Hier wartet etwas auf Dich.")

Fehlende Schlüssel und .get(...)

Greifst Du mit eckigen Klammern auf einen nicht vorhandenen Schlüssel zu, entsteht ein KeyError:

print(raum["monster"])

Mit .get(...) bekommst Du bei einem fehlenden Schlüssel stattdessen None:

monster = raum.get("monster")

print(monster)

Du kannst außerdem einen Default-Wert angeben:

monster = raum.get("monster", "Kein Monster")

print(monster)

Die Ausgabe lautet:

Kein Monster

Das ist nützlich, wenn ein fehlender Eintrag ein normaler Zustand ist und keinen Fehler darstellen soll.

Dictionaries durchlaufen

Eine for-Schleife über ein Dictionary durchläuft standardmäßig seine Schlüssel:

for schluessel in raum:
    print(schluessel)

Mit .values() erhältst Du stattdessen die Werte:

for wert in raum.values():
    print(wert)

Und mit .items() bekommst Du bei jedem Durchlauf Schlüssel und Wert gemeinsam:

for schluessel, wert in raum.items():
    print(f"{schluessel}: {wert}")

Das Unpacking funktioniert hier genauso wie beim Tupel: Schlüssel und Wert werden direkt auf die beiden Variablen verteilt.

Verschachtelte Collections

Die Werte eines Dictionaries dürfen selbst wieder Listen, Tupel, Sets oder weitere Dictionaries sein:

raum = {
    "name": "Halle",
    "ausgaenge": {
        "norden": "bibliothek",
        "osten": "krypta",
    },
    "gegenstaende": ["Fackel", "Brot"],
}

raum["ausgaenge"] liefert hier ein weiteres Dictionary:

print(raum["ausgaenge"])

Um das Ziel des nördlichen Ausgangs zu erhalten, kombinieren wir zwei Zugriffe:

print(raum["ausgaenge"]["norden"])

Die Ausgabe lautet:

bibliothek

Genau solche verschachtelten Strukturen nutzen wir gleich, um die gesamte Dungeon-Welt zu beschreiben.

Sets: ungeordnet und ohne Duplikate

Ein Set ist eine ungeordnete Collection, in der jeder Wert höchstens einmal vorkommt:

besuchte_raeume = {"halle", "bibliothek", "halle"}

print(besuchte_raeume)

Obwohl "halle" zweimal angegeben wurde, ist der Wert im Set nur einmal enthalten. Eine mögliche Ausgabe ist:

{'halle', 'bibliothek'}

Die Reihenfolge kann auch anders aussehen. Bei Sets gibt es keine garantierte Position der Elemente und deshalb auch keinen Zugriff über einen Index.

Werte hinzufügen

Mit .add(...) fügst Du einem Set einen Wert hinzu:

besuchte_raeume.add("krypta")

Fügst Du denselben Wert erneut hinzu, bleibt das Set unverändert:

besuchte_raeume.add("krypta")

"krypta" ist weiterhin nur einmal enthalten.

Mitgliedschaft prüfen

Auch bei Sets verwendest Du in:

if "krypta" in besuchte_raeume:
    print("Du warst bereits in der Krypta.")

Sets eignen sich besonders gut für Fälle, in denen Dich vor allem interessiert, ob ein Wert bereits vorhanden ist, zum Beispiel:

  • besuchte Räume
  • freigeschaltete Fähigkeiten
  • bekannte Zaubersprüche
  • einmalige IDs

Ein leeres Set anlegen

Ein leeres Set wird mit set() erzeugt:

besuchte_raeume = set()

Leere geschweifte Klammern erzeugen dagegen ein Dictionary:

leere_sammlung = {}

print(type(leere_sammlung))

Die Ausgabe lautet:

<class 'dict'>

Das ist leicht zu verwechseln, weil nicht leere Sets ebenfalls mit geschweiften Klammern geschrieben werden.

Welche Collection passt wozu?

Collection Reihenfolge Veränderbar Duplikate Zugriff
Liste feste Reihenfolge ja ja über Index
Tupel feste Reihenfolge nein ja über Index
Dictionary Einfügereihenfolge ja Schlüssel eindeutig über Schlüssel
Set keine garantierte Reihenfolge ja nein über Mitgliedschaft

Für unseren Dungeon verwenden wir:

  • ein Dictionary für den Player State
  • eine Liste für das Inventar
  • ein Dictionary für die Räume
  • ein Dictionary für die Ausgänge jedes Raums
  • eine Liste für die Gegenstände in einem Raum
  • ein Set für bereits besuchte Räume

Damit haben wir für jeden Teil unseres States eine Collection, die zur jeweiligen Aufgabe passt.

Die Dungeon-Welt als Daten

Bisher steckte die Welt direkt in der Programmlogik:

if befehl == "norden":
    print("Du stehst vor einer verschlossenen Tür.")
elif befehl == "osten":
    print("Im Osten wartet ein Goblin.")

Jeder neue Raum und jeder neue Ausgang erforderte dadurch zusätzliche Bedingungen.

Stattdessen beschreiben wir die Welt nun in einem großen Dictionary:

raeume = {
    "halle": {
        "name": "Halle",
        "beschreibung": "Du stehst in einer dunklen Halle mit moderigem Geruch.",
        "ausgaenge": {
            "norden": "bibliothek",
            "osten": "krypta",
        },
        "gegenstaende": ["Fackel"],
    },
    "bibliothek": {
        "name": "Bibliothek",
        "beschreibung": "Staubige Regale voller zerfledderter Bücher umgeben Dich.",
        "ausgaenge": {
            "sueden": "halle",
        },
        "gegenstaende": ["Schlüssel"],
    },
    "krypta": {
        "name": "Krypta",
        "beschreibung": "Feuchtigkeit glänzt auf den Wänden der alten Krypta.",
        "ausgaenge": {
            "westen": "halle",
        },
        "gegenstaende": [],
    },
}

Die Schlüssel "halle", "bibliothek" und "krypta" dienen als interne IDs der Räume. Jeder Raum ist wiederum ein Dictionary mit vier Einträgen:

  • name
  • beschreibung
  • ausgaenge
  • gegenstaende

Die Ausgänge bilden eine Richtung auf die ID des jeweiligen Zielraums ab:

"ausgaenge": {
    "norden": "bibliothek",
    "osten": "krypta",
}

Der nördliche Ausgang der Halle führt also zum Raum mit der ID "bibliothek", der östliche zur "krypta".

Damit müssen wir nicht mehr für jede mögliche Richtung einen eigenen elif-Block schreiben.

Den Player State gruppieren

Auch der Player State besteht inzwischen aus mehreren zusammengehörigen Werten:

name = "Karl"
hp = 100
max_hp = 100
gold = 50
inventar = []

Diese Werte können wir ebenfalls in einem Dictionary zusammenfassen:

spieler = {
    "name": "Karl",
    "hp": 100,
    "max_hp": 100,
    "gold": 50,
    "inventar": [],
}

Der Zugriff erfolgt nun über die jeweiligen Schlüssel:

print(spieler["name"])
print(spieler["hp"])
print(spieler["inventar"])

Damit ist auch im Code sofort erkennbar, welche Werte zum Player State gehören.

Der Dungeon, Schritt 4

Öffne dungeon.py und ersetze den bisherigen Inhalt durch folgenden Code:

# Dungeon – Teil 4: Collections und Welt als Daten
raeume = {
    "halle": {
        "name": "Halle",
        "beschreibung": "Du stehst in einer dunklen Halle mit moderigem Geruch.",
        "ausgaenge": {
            "norden": "bibliothek",
            "osten": "krypta",
        },
        "gegenstaende": ["Fackel"],
    },
    "bibliothek": {
        "name": "Bibliothek",
        "beschreibung": "Staubige Regale voller zerfledderter Bücher umgeben Dich.",
        "ausgaenge": {
            "sueden": "halle",
        },
        "gegenstaende": ["Schlüssel"],
    },
    "krypta": {
        "name": "Krypta",
        "beschreibung": "Feuchtigkeit glänzt auf den Wänden der alten Krypta.",
        "ausgaenge": {
            "westen": "halle",
        },
        "gegenstaende": [],
    },
}

print("Du stehst vor dem rostigen Tor eines vergessenen Verlieses.")
print("Ein kalter Luftzug weht Dir entgegen.")

spieler = {
    "name": input("Wie heißt Du, Abenteurer? ").strip(),
    "hp": 100,
    "max_hp": 100,
    "gold": int(input("Wie viele Goldmünzen bringst Du mit? ")),
    "inventar": [],
}

ort = "halle"
besuchte_raeume = set()
raum_anzeigen = True

print()
print(f"Willkommen, {spieler['name']}!")
print("Befehle: umsehen, nimm, inventar, status, ende")

while True:
    raum = raeume[ort]

    if raum_anzeigen:
        besuchte_raeume.add(ort)

        print()
        print(raum["beschreibung"])
        print("Ausgänge:", ", ".join(raum["ausgaenge"]))

        raum_anzeigen = False

    befehl = input("\n> ").strip().lower()

    if befehl == "ende":
        print("Du verlässt das Verlies. Bis zum nächsten Mal.")
        break

    elif befehl == "umsehen":
        if raum["gegenstaende"]:
            print("Hier liegt:", ", ".join(raum["gegenstaende"]))
        else:
            print("Hier liegt nichts Brauchbares.")

    elif befehl == "nimm":
        if raum["gegenstaende"]:
            gefundene_gegenstaende = ", ".join(raum["gegenstaende"])

            spieler["inventar"].extend(raum["gegenstaende"])
            raum["gegenstaende"].clear()

            print(f"Du nimmst: {gefundene_gegenstaende}")
        else:
            print("Hier gibt es nichts zu nehmen.")

    elif befehl == "inventar":
        if spieler["inventar"]:
            print("Inventar:", ", ".join(spieler["inventar"]))
        else:
            print("Dein Inventar ist leer.")

    elif befehl == "status":
        print(
            f"{spieler['name']} | "
            f"HP: {spieler['hp']}/{spieler['max_hp']} | "
            f"Gold: {spieler['gold']}"
        )
        print(f"Besuchte Räume: {len(besuchte_raeume)}")
        print(f"Gegenstände: {len(spieler['inventar'])}")

    elif befehl in raum["ausgaenge"]:
        ort = raum["ausgaenge"][befehl]
        raum_anzeigen = True

    else:
        print("Das geht hier nicht.")
        print("Mögliche Ausgänge:", ", ".join(raum["ausgaenge"]))
        print("Weitere Befehle: umsehen, nimm, inventar, status, ende")

Führe das Programm aus:

python dungeon.py

Ein möglicher Durchlauf sieht so aus:

Du stehst vor dem rostigen Tor eines vergessenen Verlieses.
Ein kalter Luftzug weht Dir entgegen.
Wie heißt Du, Abenteurer? Karl
Wie viele Goldmünzen bringst Du mit? 50

Willkommen, Karl!
Befehle: umsehen, nimm, inventar, status, ende

Du stehst in einer dunklen Halle mit moderigem Geruch.
Ausgänge: norden, osten

> umsehen
Hier liegt: Fackel

> nimm
Du nimmst: Fackel

> norden

Staubige Regale voller zerfledderter Bücher umgeben Dich.
Ausgänge: sueden

> nimm
Du nimmst: Schlüssel

> inventar
Inventar: Fackel, Schlüssel

> status
Karl | HP: 100/100 | Gold: 50
Besuchte Räume: 2
Gegenstände: 2

> ende
Du verlässt das Verlies. Bis zum nächsten Mal.

Was im neuen Game-Loop passiert

Der Player State steckt nun in einem Dictionary:

spieler = {
    "name": input("Wie heißt Du, Abenteurer? ").strip(),
    "hp": 100,
    "max_hp": 100,
    "gold": int(input("Wie viele Goldmünzen bringst Du mit? ")),
    "inventar": [],
}

Das Inventar ist eine Liste innerhalb dieses Dictionaries:

spieler["inventar"]

Wir kombinieren also bereits zwei Collections: Ein Dictionary gruppiert den Player State, während eine Liste die Gegenstände des Spielers speichert.

Der aktuelle Raum steht weiterhin in der Variablen ort:

ort = "halle"

Mit diesem Wert holen wir das Dictionary des aktuellen Raums aus unserer Welt:

raum = raeume[ort]

Enthält ort den String "halle", entspricht das diesem Zugriff:

raum = raeume["halle"]

raum verweist anschließend auf das Dictionary der Halle. Wenn wir dessen Inhalte verändern, verändern wir damit auch den entsprechenden Raum innerhalb von raeume.

Bereits besuchte Räume speichern

Das Set besuchte_raeume beginnt leer:

besuchte_raeume = set()

Sobald ein Raum angezeigt wird, fügen wir seine ID hinzu:

besuchte_raeume.add(ort)

Betritt der Spieler denselben Raum später erneut, bleibt dessen ID trotzdem nur einmal im Set. Mit len(...) erhalten wir deshalb die Anzahl der unterschiedlichen Räume, die bereits besucht wurden.

Räume nur nach einem Wechsel anzeigen

Die bool-Variable raum_anzeigen bestimmt, ob die Beschreibung des aktuellen Raums ausgegeben werden soll:

raum_anzeigen = True

Nach der Ausgabe setzen wir sie auf False:

raum_anzeigen = False

Dadurch erscheint die Raumbeschreibung nicht nach jedem einzelnen Befehl erneut.

Wechselt der Spieler in einen anderen Raum, setzen wir die Variable wieder auf True:

ort = raum["ausgaenge"][befehl]
raum_anzeigen = True

Beim nächsten Durchlauf des Game-Loops wird das Dictionary des neuen Raums geladen und dessen Beschreibung ausgegeben.

Gültige Richtungen erkennen

Dieser Ausdruck prüft, ob der eingegebene Befehl einem Schlüssel im Ausgangs-Dictionary entspricht:

elif befehl in raum["ausgaenge"]:

Befindet sich der Spieler in der Halle, enthält dieses Dictionary folgende Einträge:

{
    "norden": "bibliothek",
    "osten": "krypta",
}

Die Befehle "norden" und "osten" sind dort deshalb gültig.

Das Ziel der gewählten Richtung erhalten wir direkt über den Schlüssel:

ort = raum["ausgaenge"][befehl]

Gibt der Spieler "norden" ein, entspricht das:

ort = raum["ausgaenge"]["norden"]

Der neue Wert von ort lautet anschließend "bibliothek".

Damit braucht die Bewegung durch den Dungeon keine eigene Bedingung mehr für jede einzelne Richtung. Dieselbe Zeile funktioniert für alle Ausgänge, die im jeweiligen Raum definiert sind.

Gegenstände aufnehmen

Die Gegenstände des aktuellen Raums liegen in einer Liste:

raum["gegenstaende"]

Mit extend(...) übernehmen wir alle Elemente dieser Liste in das Inventar:

spieler["inventar"].extend(raum["gegenstaende"])

Danach leeren wir die Liste im Raum:

raum["gegenstaende"].clear()

Die Gegenstände befinden sich jetzt im Inventar und liegen nicht mehr im Raum. Der Spieler kann sie deshalb nicht noch einmal aufnehmen.

Unser Befehl nimm nimmt momentan übrigens alle Gegenstände eines Raums auf einmal. Eine Auswahl einzelner Gegenstände bauen wir später ein.

Listen als String ausgeben

Die Methode join(...) verbindet mehrere Strings zu einem gemeinsamen String:

", ".join(spieler["inventar"])

Aus

["Fackel", "Schlüssel"]

wird dadurch:

Fackel, Schlüssel

Der String vor .join(...) bestimmt, was zwischen die einzelnen Elemente gesetzt wird. Hier ist das ein Komma gefolgt von einem Leerzeichen.

join(...) erwartet, dass die zu verbindenden Elemente Strings sind. Eine Liste mit Zahlen funktioniert deshalb nicht direkt:

", ".join([1, 2, 3])

Das würde zu einem TypeError führen.

Daten und Logik trennen

Der wichtigste Schritt in diesem Teil ist weniger eine einzelne Methode als die neue Struktur unseres Programms.

Die Räume werden als Daten beschrieben:

raeume = {
    ...
}

Der Game-Loop enthält dagegen die Logik, die mit diesen Daten arbeitet:

while True:
    ...

Möchtest Du einen weiteren Raum hinzufügen, musst Du nicht mehr für jede seiner Richtungen neue Bedingungen in den Game-Loop einbauen. Du ergänzt den Raum im Dictionary raeume und verbindest ihn über seine Ausgänge mit der vorhandenen Welt.

Unser Dungeon ist damit noch lange keine fertige Game Engine. Aber Welt und Programmlogik sind jetzt deutlich weniger eng miteinander verdrahtet. Das macht die nächsten Erweiterungen wesentlich angenehmer.

Stolperfallen

  • Beim Index bei 0 zu zählen vergessen: Eine Liste mit drei Elementen besitzt die Indizes 0, 1 und 2. Ein Zugriff auf inventar[3] führt zu einem IndexError.

  • Einen Wert mit remove(...) entfernen, der nicht existiert: Findet remove(...) den angegebenen Wert nicht in der Liste, entsteht ein ValueError.

  • Einen fehlenden Dictionary-Schlüssel verwenden: Ein Zugriff wie raum["monster"] führt zu einem KeyError, wenn der Schlüssel nicht existiert. Mit raum.get("monster") erhältst Du stattdessen None.

  • Liste und Dictionary verwechseln: Auf Listen greifst Du mit numerischen Indizes zu, auf Dictionaries mit ihren Schlüsseln.

python inventar[0] raum["beschreibung"]

  • append(...) und extend(...) verwechseln: append(...) fügt einen Wert als neues Element hinzu. extend(...) übernimmt mehrere Elemente in die bestehende Liste.

  • Ein leeres Set mit {} anlegen: {} erzeugt ein leeres Dictionary. Für ein leeres Set benötigst Du set().

  • Von einer festen Set-Reihenfolge ausgehen: Sets besitzen keine verlässliche Reihenfolge für Deine Programmlogik. Verwende eine Liste oder ein Tupel, wenn die Position der Elemente wichtig ist.

  • Dictionary-Reihenfolge mit Sortierung verwechseln: Dictionaries behalten ihre Einfügereihenfolge bei, sortieren ihre Schlüssel aber nicht automatisch.

  • Zahlen direkt mit join(...) verbinden: join(...) erwartet Strings. ", ".join([1, 2, 3]) führt deshalb zu einem TypeError.

  • Umlaute in Befehlen übersehen: Im Code verwenden wir für die südliche Richtung bewusst "sueden", damit sie ohne Umlaut eingegeben werden kann.

Übungen

1. Eine Schatzkammer ergänzen

Füge dem Verlies einen vierten Raum namens "schatzkammer" hinzu. Er soll von der Bibliothek aus nach Norden erreichbar sein und einen "Goldhaufen" enthalten.

Lösung Ergänze das Dictionary `raeume` um folgenden Eintrag:
"schatzkammer": {
    "name": "Schatzkammer",
    "beschreibung": "Gold und Edelsteine glitzern im Licht Deiner Fackel.",
    "ausgaenge": {
        "sueden": "bibliothek",
    },
    "gegenstaende": ["Goldhaufen"],
},
Erweitere außerdem die Ausgänge der Bibliothek:
"ausgaenge": {
    "sueden": "halle",
    "norden": "schatzkammer",
},
Die Bibliothek führt nun nach Norden in die Schatzkammer. Von dort gelangt der Spieler nach Süden zurück zur Bibliothek.

2. Einen Startgegenstand hinzufügen

Gib dem Spieler zu Beginn ein Stück Brot. Das Inventar soll also nicht mehr leer starten.

Lösung Ändere den Eintrag `inventar` im Player State:
spieler = {
    "name": input("Wie heißt Du, Abenteurer? ").strip(),
    "hp": 100,
    "max_hp": 100,
    "gold": int(input("Wie viele Goldmünzen bringst Du mit? ")),
    "inventar": ["Brot"],
}
Der Befehl `inventar` zeigt das Brot anschließend direkt an.

3. Alle Ausgänge eines Raums ausgeben

Schreibe eine for-Schleife, die alle Richtungen und Zielräume der Halle ausgibt.

Die Ausgabe soll ungefähr so aussehen:

norden -> bibliothek
osten -> krypta
Lösung
halle = raeume["halle"]

for richtung, ziel in halle["ausgaenge"].items():
    print(f"{richtung} -> {ziel}")
`.items()` liefert bei jedem Durchlauf Schlüssel und Wert. Beide werden direkt in die Variablen `richtung` und `ziel` entpackt.

Im nächsten Teil

Die Welt ist jetzt als Daten beschrieben. Der Game-Loop ist durch die vielen Befehle allerdings weiterhin recht lang.

In Teil 5 lernst Du deshalb Funktionen kennen. Damit zerlegen wir die Logik in benannte, wiederverwendbare Blöcke und machen den Game-Loop wieder übersichtlicher.

0 Kommentare

Noch keine Kommentare. Sei der/die Erste!