Jeden Zug nach den Spielfeldregeln ausführen

C++Beginner
Jetzt üben

Einführung

Implementiere erlaubte Schritte und das Schieben einzelner Kisten in einem spielbaren C++-Lagerspiel für das Terminal.

Du arbeitest an einem kleinen Terminalspiel mit festem Gelände und getrennten beweglichen Objekten. Du implementierst die zentralen C++-Funktionen und spielst anschließend, um ihr Verhalten zu prüfen.

Gehen, ohne Wände zu durchqueren

In diesem Schritt spielst du ein fertiges Beispiel und sorgst dann dafür, dass sich dein eigener Spieler an die Spielfeldregeln hält. Beginne im Tab Terminal. Wenn du den WebIDE-Editor nutzt, öffne Terminal > New Terminal; alle folgenden Befehle werden in dieser Shell ausgeführt. cd ändert das Arbeitsverzeichnis dieser Shell; die Änderung gilt nicht für neue Terminalsitzungen.

cd /home/labex/project/sokoban

Das vorbereitete Programm sample ist eine vollständige Demonstration und von deinem unfertigen Programm getrennt. ./ führt ein Programm aus dem aktuellen Verzeichnis aus.

./sample

Gib d ein, drücke Enter, gib erneut d ein und drücke Enter. Diese zwei Züge führen den Spieler zur Kiste und schieben sie auf das Ziel. Solved! erscheint, und $ wird zu *. Gib u ein und drücke Enter: Spieler und Kiste kehren auf ihre vorherigen Felder zurück, und die Meldung wird zu Not solved. Gib q ein und drücke Enter, um zur Shell zurückzukehren. Jede Zeile enthält nur einen Buchstaben; leere oder unbekannte Befehle zeigen Hilfe an und ändern nichts. Das Beispiel zeigt das gewünschte Ergebnis; sein Starten vervollständigt deinen eigenen Spielkern nicht.

Die vorbereitete Klasse Game ist in engine.hpp deklariert. Point enthält die ganzzahligen Koordinaten x und y: x wächst nach rechts, y nach unten. State enthält den Spieler und einen std::vector<Point> mit allen Kisten. terrain[y][x] enthält nur Wände #, Leerzeichen und Ziele .. Durch const ist dieses Gelände nach der Konstruktion schreibgeschützt; eine bewegte Kiste kann daher kein Ziel löschen. Die Darstellung kombiniert beide Ebenen: * bedeutet Kiste auf einem Ziel, + Spieler auf einem Ziel.

floor(point) meldet, ob eine Koordinate innerhalb des Spielfelds liegt und keine Wand ist. boxAt(point) liefert den Vektorindex einer Kiste oder -1, wenn dort keine steht. Diese Hilfsfunktionen sowie Laden, Darstellung und Befehlseingabe sind vorbereitet. Du implementierst move, won und später undo in engine.cpp; erhalte engine.hpp, support.cpp, main.cpp, levels/ und sample. Du darfst Hilfsfunktionen hinzufügen und gleichwertige Algorithmen verwenden. Die Prüfung bewertet Verhalten, keine bestimmte Schreibweise oder Zugfolge.

Lies die vorbereitete Deklaration und deinen Ausgangscode:

cat engine.hpp
cat engine.cpp

Das Ausgangsprogramm lehnt jeden Zug ab. Deshalb gibt dieser beabsichtigte Selbsttest FAIL walk changes only the player aus und endet mit einem Status ungleich null; das bedeutet, dass noch Arbeit fehlt, nicht dass die Umgebung defekt ist:

bash /opt/labex-sokoban/check.sh walk

Unterstützte Karten sind umschlossene rechteckige ASCII-Raster mit mindestens drei Zeilen und drei Spalten. Sie enthalten genau einen Spieler, mindestens eine Kiste und genauso viele Ziele wie Kisten, ohne überlappende Objekte. Die Zeichen sind # für Wand, Leerzeichen für Boden, . für Ziel, @ für Spieler, $ für Kiste, + für Spieler auf Ziel und * für Kiste auf Ziel. Eine Karte darf unlösbar oder bereits zu Beginn gelöst sein. Der vorbereitete Lader lehnt andere Eingaben ab; du musst keinen Parser schreiben.

move(dx, dy) akzeptiert nur (1,0), (-1,0), (0,1) oder (0,-1). Jedes andere Ganzzahlpaar muss false zurückgeben, ohne Zustand oder Verlauf zu ändern. Eine erlaubte Aktion liefert true; eine unerlaubte liefert false und ändert nichts. Für die Feldbelegung ist die Reihenfolge im Kistenvektor unerheblich. Später stellt Rückgängig den tatsächlich vor der Aktion gespeicherten vollständigen Zustand wieder her, einschließlich der damaligen Kistenreihenfolge.

Beginne nur mit dem Gehen. Point{...} erzeugt einen Koordinatenwert. && verlangt beide Bedingungen; bei || genügt eine davon. Wir führen die vier Richtungen ausdrücklich auf, bevor Koordinaten addiert werden; dadurch werden auch ungewöhnlich große Ganzzahlen sicher abgelehnt. >= 0 kennzeichnet ein von einer Kiste besetztes Feld. Zunächst blockieren Kisten das Gehen; Schieben kommt im nächsten Schritt. Die Zweige mit return false lassen den gesamten Zustand unverändert.

Um die vollständige Datei im Terminal zu schreiben, leitet cat > engine.cpp Text in diese Datei um und ersetzt ihren alten Inhalt. Der Here-Document-Marker in Anführungszeichen <<'CPP' lässt die Shell alle folgenden Zeilen wörtlich übernehmen, bis eine Zeile nur CPP enthält. Kopiere den gesamten Befehl einschließlich des Abschlussmarkers:

cat > engine.cpp <<'CPP'
#include "engine.hpp"

bool Game::move(int dx, int dy) {
    if (!((dx == -1 && dy == 0) || (dx == 1 && dy == 0) ||
          (dx == 0 && dy == -1) || (dx == 0 && dy == 1))) return false;
    Point next{state.player.x + dx, state.player.y + dy};
    if (!floor(next) || boxAt(next) >= 0) return false;
    state.player = next;
    return true;
}
bool Game::undo() { return false; }
bool Game::won() const { return false; }
CPP

Kompiliere die drei Quelldateien. -std=c++17 wählt C++17, -Wall -Wextra aktiviert nützliche Warnungen und -o sokoban benennt die ausführbare Datei. Compilerfehler enthalten eine Zeilennummer; korrigiere diese Zeile in engine.cpp oder wiederhole das vollständige Ersetzen und führe denselben Build erneut aus.

g++ -std=c++17 -Wall -Wextra engine.cpp support.cpp main.cpp -o sokoban
./sokoban

Gib a und dann Enter ein: Die linke Wand führt zu Blocked, das Spielfeld bleibt unverändert. Gib d und dann Enter ein: Der Spieler geht nach rechts. Ein zweites d wird weiterhin von der Kiste blockiert, wie in diesem Schritt erwartet. u meldet No earlier move: Rückgängig ist noch nicht implementiert. Gib q zum Beenden ein.

bash /opt/labex-sokoban/check.sh walk

All walk checks passed bestätigt das Gehen in vier Richtungen und sichere Ablehnungen. Die Prüfungen kompilieren deinen tatsächlichen Spielkern in einem temporären Verzeichnis und führen auch deine vorhandene ausführbare Datei sokoban in isolierten Spielen aus. Erstelle sokoban nach Änderungen neu; eine fehlende oder veraltete Programmdatei ist keine vollständige Abgabe. Die Prüfungen bewegen deine laufende Partie nicht und ändern deinen Quelltext nicht.

Eine Kiste schieben und das Ziel erhalten

In diesem Schritt erweiterst du den Spielkern um das Schieben einer Kiste und leitest die Lösung aus ihrer tatsächlichen Position ab. Arbeite weiter in Terminal, im Verzeichnis /home/labex/project/sokoban. Jeder Start beginnt ein neues Spiel im Speicher; das Bearbeiten oder Neubauen des Programms speichert keine laufende Partie.

Prüfe vor dem Schieben das Feld hinter der Kiste. Es muss Boden oder Ziel sein und darf keine andere Kiste enthalten. Lehne zuerst ungültige Fälle ab und ändere danach Kiste und Spieler gemeinsam. Das verhindert Schieben durch Wände oder das gemeinsame Schieben zweier Kisten. Der Kistenindex ist ein int, weil -1 das Fehlen einer Kiste bedeutet; verwende ihn erst nach der Prüfung box >= 0 als Vektorindex.

won() const beobachtet das Spiel, ohne es zu verändern. Die bereichsbasierte Schleife for (Point box : state.boxes) besucht die Koordinaten jeder Kiste als Wert. Jede Kiste muss auf einem . im festen Gelände stehen; bei zwei Kisten reicht eine richtig platzierte Kiste nicht. Ein leerer Kistenvektor gilt nach unserem Vertrag nie als gelöstes Spiel. Der Sieg wird jedes Mal neu berechnet und wird daher wieder falsch, wenn eine Kiste ihr Ziel verlässt. In diesem Lab lehnt undo weiterhin bewusst ab; der Verlauf ist die neue Aufgabe des nächsten Labs.

Ersetze engine.cpp durch die vollständige Implementierung für Bewegung und Lösungsprüfung:

cat > engine.cpp <<'CPP'
#include "engine.hpp"

bool Game::move(int dx, int dy) {
    if (!((dx == -1 && dy == 0) || (dx == 1 && dy == 0) ||
          (dx == 0 && dy == -1) || (dx == 0 && dy == 1))) return false;
    Point next{state.player.x + dx, state.player.y + dy};
    if (!floor(next)) return false;
    int box = boxAt(next);
    Point beyond{next.x + dx, next.y + dy};
    if (box >= 0 && (!floor(beyond) || boxAt(beyond) >= 0)) return false;
    if (box >= 0) state.boxes[box] = beyond;
    state.player = next;
    return true;
}
bool Game::undo() {
    return false;
}
bool Game::won() const {
    if (state.boxes.empty()) return false;
    for (Point box : state.boxes) {
        if (terrain[box.y][box.x] != '.') return false;
    }
    return true;
}
CPP
g++ -std=c++17 -Wall -Wextra engine.cpp support.cpp main.cpp -o sokoban
./sokoban

Gib d, d ein, jeweils gefolgt von Enter. Das Spielfeld zeigt * und Solved!. Gib noch ein d ein: Die Kiste verlässt das Ziel, der Spieler steht als + darauf und das Spiel zeigt wieder Not solved. Ein weiteres d wird blockiert, weil die Kiste in die Wand geraten würde. Das feste Ziel existiert weiterhin unter dem Spieler. Gib q ein.

Probiere das vorbereitete Lager mit zwei Kisten aus, indem du seinen Dateipfad als optionales Programmargument übergibst:

./sokoban levels/warehouse.txt

Eine mögliche Zugfolge ist w, s, d, d, w, jeweils mit Enter. Du darfst eine andere erlaubte Folge wählen; die kürzeste Lösung ist nicht erforderlich. Beide Kisten müssen als * erscheinen, bevor Solved! angezeigt wird. Gib q ein.

Ein falsch geschriebener Levelpfad führt zu Cannot open level: ... und beendet das Programm, ohne eine Leveldatei zu ändern. Führe Folgendes aus, um diesen gewöhnlichen Fehler absichtlich zu sehen:

./sokoban levels/missing.txt

Zur Korrektur wiederholst du den vorherigen Befehl mit levels/warehouse.txt. Es geht nur um den Dateinamen; die Umgebung muss nicht zurückgesetzt werden. Ein erneuter Levelstart beginnt ein neues Spiel im Speicher.

bash /opt/labex-sokoban/check.sh move

All move checks passed umfasst erlaubtes Gehen und Schieben, ungültige Aktionen, erhaltene Ziele, eine bereits gelöste Karte und verschiedene Lösungen mit zwei Kisten. Behalte den Spielkern und die vorbereiteten Dateien; dein Bewegungssystem ist jetzt spielbar.

Zusammenfassung

Du hast festes Gelände von beweglichen Objekten getrennt, ungültige Aktionen ohne Nebenwirkungen abgelehnt und zwei kleine Lager gelöst. Das nächste eigenständige Lab ergänzt Verlauf und Rückgängig-Funktion.