Dungeon Treasure Game – Aufgabenblatt

Start Template: Exercise - Browser Treasure Game JS

Die Karte besteht aus einem zweidimensionalen Array. Jedes Feld hat einen dieser Werte:

WertBedeutung
"."Freier Boden
"S"Startposition
"#"Wand
"T"Schatz
"X"Falle
"E"Ausgang

Aufgabe 0: Code Analyse

Mache dich mit dem Code im Start Template vertraut und versuche alles zu verstehen.

Du wirst merken, dass schon ein Event-Listener auf das Drücken von Pfeiltasten reagiert, aber noch nichts passiert.

Versuche die Funktion movePlayer anzupassen, sodass die Position aktualisiert wird:

function movePlayer(rowChange, colChange) {
	player.row += rowChange;
	player.col += colChange;
}

Teste dein Spiel indem du die Datei neu abspeicherst und im Browser neu lädst.

Was funktioniert schon?
Was funktioniert noch nicht? \

Aufgabe 1: Spielinformationen anzeigen

Ziel: Die Info-Box soll aktuelle Lebenspunkte, Punkte und verbleibende Schätze anzeigen. Außerdem soll der Spieler sich auf dem Bildschirm bewegen.

Was ist zu tun?

  1. Implementiere countTreasures() – zählt alle "T"-Felder in der Karte und gibt die Anzahl zurück.
  2. Implementiere updateInfo() – aktualisiert die HTML-Elemente #health, #score und #treasuresLeft.
  3. Implementiere showMessage(text) – setzt den Text des #message-Elements.
  4. Ergänze movePlayer(): Ruf am Ende showMessage("You moved."), updateInfo() und renderMap() auf.
  5. Ergänze restartGame(): Setze player.row = 0 und player.col = 0 zurück und ruf showMessage(...) mit einem Starttext auf.

Hinweise

  • Mit document.getElementById("health") bekommst du das HTML-Element mit der ID health.
  • Mit .textContent = wert setzt du den sichtbaren Text eines Elements.
  • countTreasures() braucht zwei verschachtelte Schleifen: eine über die Zeilen (for (let row of map)), eine über die Felder jeder Zeile.
  • Die Karte muss nach jeder Bewegung neu gezeichnet werden, damit der Spieler sich bewegt – das macht renderMap().

Aufgabe 2: Kartengrenzen überprüfen

Ziel: Der Spieler soll nicht über den Rand der Karte hinaus laufen können.

Was ist zu tun?

  1. Schreibe eine Hilfsfunktion isInsideMap(row, col), die true zurückgibt, wenn die Koordinaten innerhalb der Karte liegen.
  2. Ändere movePlayer(): Berechne zuerst newRow und newCol, prüfe dann mit isInsideMap, ob die neue Position gültig ist – erst danach aktualisiere player.row und player.col.

Hinweise

  • Eine Position ist gültig, wenn row >= 0 && row < map.length && col >= 0 && col < map[0].length.
  • Wenn die Position ungültig ist: showMessage(...) mit einer passenden Meldung aufrufen und die Funktion mit return abbrechen.
  • player.row = newRow und player.col = newCol kommen erst ganz am Schluss, wenn alle Prüfungen bestanden sind.

Aufgabe 3: Wände überprüfen

Ziel: Der Spieler soll nicht durch Wände laufen können.

Was ist zu tun?

Ergänze movePlayer(): Lese nach der Grenzenprüfung das Zielfeld aus der Karte aus und prüfe, ob es eine Wand ist.

Hinweise

  • Mit map[newRow][newCol] bekommst du den Inhalt des Zielfelds.
  • Wände haben den Wert "#".
  • Wenn das Zielfeld eine Wand ist: showMessage(...) aufrufen und mit return abbrechen.

Aufgabe 4: Reichhaltigere Karte gestalten

Ziel: Die Funktion generateMaze() soll eine interessantere, handgefertigte Karte zurückgeben – mit mehreren Schätzen, Fallen und Wänden.

Was ist zu tun?

Erweitere den Inhalt von generateMaze(): Füge mehrere Wände, Schätze und Fallen hinzu. Die Karte muss weiterhin lösbar sein.

Hinweise

  • Zeilen und Spalten beginnen bei Index 0. Die Karte ist 16×16 (Indizes 0–15).
  • Einzelne Felder setzt du mit: map[zeile][spalte] = "#" (oder "T", "X" usw.)
  • Eine ganze Reihe Wände setzt du mit einer Schleife: for (let col = 2; col <= 10; col++) map[4][col] = "#";
  • Denk dir Durchgänge (Lücken in Wandreihen), damit du noch zu allen Schätzen und zum Ausgang kommst.
  • Spiele die Karte durch, bevor du mit der nächsten Aufgabe weitermachst.

Aufgabe 5: Feldaktionen implementieren

Ziel: Wenn der Spieler ein bestimmtes Feld betritt, soll etwas passieren.

Was ist zu tun?

  1. Schreibe eine neue Funktion handleTile(tile, row, col) mit folgendem Verhalten:
    • "T" → Punkte erhöhen (player.score += 10), Feld aus der Karte entfernen, Meldung anzeigen.
    • "X" → Lebenspunkte verringern (player.health -= 1), Feld aus der Karte entfernen, Meldung anzeigen.
    • "E" → Prüfen, ob alle Schätze eingesammelt wurden; passende Meldung anzeigen.
    • Alle anderen Felder → "You moved." anzeigen.
  2. Rufe handleTile(targetTile, newRow, newCol) in movePlayer() auf (nach der Bewegung, anstelle des bisherigen showMessage("You moved.")).

Hinweise

  • Ein Feld entfernst du, indem du es durch freien Boden ersetzt: map[row][col] = ".".
  • Ob alle Schätze weg sind, prüfst du mit countTreasures() === 0.

Aufgabe 6: Spielende erkennen

Ziel: Das Spiel soll enden, wenn der Spieler alle Herzen verliert oder den Ausgang mit allen Schätzen erreicht.

Was ist zu tun?

  1. Schreibe eine neue Funktion checkGameState():
    • Wenn player.health <= 0: gameOver = true setzen und eine Verlieren-Meldung anzeigen.
    • Wenn der Spieler auf "E" steht und keine Schätze mehr übrig sind: gameOver = true setzen und eine Gewinnen-Meldung anzeigen.
  2. Ergänze movePlayer():
    • Ganz am Anfang: Wenn gameOver gesetzt ist, sofort mit return abbrechen.
    • Ganz am Ende: checkGameState() aufrufen.

Hinweise

  • Das aktuelle Feld des Spielers: map[player.row][player.col].
  • gameOver ist eine globale Variable – wenn du sie auf true setzt, verhindert die Prüfung am Anfang von movePlayer() weitere Eingaben.

Aufgabe 7: Einfache zufällige Karte

Ziel: Bei jedem Neustart soll eine andere Karte erzeugt werden – zunächst ohne Wände.

Was ist zu tun?

  1. Schreibe eine Hilfsfunktion shuffleArray(array), die ein Array zufällig durchmischt.
  2. Ersetze den Inhalt von generateMaze() durch eine neue Version:
    • Erstelle eine Liste aller freien Felder (außer der Startposition [0][0]).
    • Mische die Liste mit shuffleArray.
    • Platziere Ausgang, Schätze und Fallen, indem du jeweils ein Element vom Ende der Liste nimmst.

Hinweise

  • Fisher-Yates-Mischen: Gehe mit einem Index i von hinten nach vorne durch das Array. Tausche array[i] mit einem zufälligen Element an einem Index zwischen 0 und i.
  • Math.floor(Math.random() * (i + 1)) ergibt eine zufällige Ganzzahl von 0 bis einschließlich i.
  • array.pop() entfernt das letzte Element und gibt es zurück – praktisch zum zufälligen Entnehmen nach dem Mischen.
  • Problem, das du dabei entdeckst: Schätze und der Ausgang können hinter zufällig platzierten Wänden (spätere Aufgabe) eingeschlossen sein. Das lösen wir in Aufgabe 8.

Aufgabe 8: Zufällige Wände mit Erreichbarkeitsprüfung

Ziel: Wände sollen zufällig hinzugefügt werden, aber nur dann, wenn danach noch alle Felder erreichbar sind.

Was ist zu tun?

  1. Schreibe getReachableSet(map, startRow, startCol): Findet alle von [startRow][startCol] aus erreichbaren Felder per Breitensuche (BFS). Gibt ein Set mit Koordinaten-Strings (z.B. "3,7") zurück.
  2. Schreibe allWalkableTilesReachable(map): Gibt true zurück, wenn jedes Nicht-Wand-Feld im Set von getReachableSet(map, 0, 0) enthalten ist.
  3. Ergänze generateMaze(): Mische die restlichen freien Felder und versuche, jedes als Wand zu setzen. Behalte die Wand nur, wenn allWalkableTilesReachable(map) noch true zurückgibt.

Hinweise

  • BFS-Grundprinzip: Starte mit einer Warteschlange, die nur den Startpunkt enthält. Nimm immer das erste Element heraus, prüfe alle 4 Nachbarn – falls begehbar und noch nicht besucht, füge sie zur Warteschlange hinzu.
  • Koordinaten als String: `${row},${col}` (Template-Literal).
  • new Set() erstellt eine leere Menge, .add(key) fügt einen Wert hinzu, .has(key) prüft ob er enthalten ist.
  • Wenn eine Wand die Erreichbarkeit zerstört: map[row][col] = "." rückgängig machen.

Aufgabe 9 (Bonus): Keine 2×2-Wandblöcke

Ziel: Klobige 2×2-Wandblöcke sollen vermieden werden, damit die Karte besser und spannender aussieht.

Was ist zu tun?

  1. Schreibe createsWallCluster(map, row, col): Gibt true zurück, wenn durch Setzen einer Wand an [row][col] ein vollständiges 2×2-Wandquadrat entsteht.
  2. Füge in generateMaze() beim Wand-Platzieren eine zusätzliche Bedingung hinzu: Wand nur behalten, wenn createsWallCluster false zurückgibt.

Hinweise

  • Es gibt 4 mögliche 2×2-Quadrate, in denen [row][col] vorkommt. Definiere alle vier als Listen von je 4 Zell-Koordinaten.
  • Für jedes Quadrat: Prüfe zuerst, ob alle 4 Felder innerhalb der Karte liegen. Zähle dann die Wände. Wenn alle 4 Wände sind, gibt es ein Cluster.
  • Da map[row][col] beim Aufruf bereits auf "#" gesetzt ist, zählt es beim Überprüfen mit.

Aufgabe 10 (Bonus): Lösbarkeit trotz Fallen prüfen

Ziel: Die Karte soll immer lösbar sein – auch wenn man bedenkt, dass Fallen Lebenspunkte kosten.

Was ist zu tun?

  1. Schreibe getPermutations(array): Gibt alle möglichen Reihenfolgen eines Arrays zurück (rekursiv).
  2. Schreibe minTrapCostBetween(map, start, end): Findet den Weg zwischen zwei Punkten, der am wenigsten Fallen enthält (Dijkstra-Algorithmus). Gibt die minimale Fallenzahl zurück.
  3. Schreibe canSolveWithHealth(map, health): Probiert alle Reihenfolgen, alle Schätze einzusammeln und dann den Ausgang zu erreichen. Gibt true zurück, wenn mindestens eine Reihenfolge existiert, bei der die gesamten Fallenkosten kleiner als health sind.
  4. Ergänze die Wand-Platzierung in generateMaze(): Wand nur behalten, wenn canSolveWithHealth(map, 3) noch true zurückgibt.

Hinweise

  • Dijkstra ist wie BFS, aber mit Priorität: Immer das Element mit den geringsten bisherigen Kosten zuerst bearbeiten. Normale Felder kosten 0, Fallen kosten 1.
  • Infinity als Startwert für Kosten – ein Feld wurde noch nicht erreicht.
  • getPermutations gibt für ein leeres Array [[]] zurück (eine Permutation: die leere).
  • Die Reihenfolge: Start → Schatz₁ → Schatz₂ → … → Ausgang. Teste alle Reihenfolgen der Schätze.