Restaurer l’état précédent complet

C++Beginner
Pratiquer maintenant

Introduction

Ajoutez des instantanés complets par valeur et une annulation sûre de plusieurs coups à un jeu Sokoban en C++.

Vous travaillerez sur un petit jeu de terminal dont le terrain fixe est séparé des objets mobiles. Vous implémenterez les fonctions C++ centrales, puis jouerez pour vérifier leur comportement.

Enregistrer l’état avant un déplacement autorisé

Dans cette étape, enregistrez des états antérieurs complets afin que l’annulation puisse ensuite inverser la marche et les poussées. Ce laboratoire initialise ses propres fichiers et n’a pas besoin de ceux du précédent. Commencez dans l’onglet Terminal, ou utilisez Terminal > New Terminal dans WebIDE.

cd /home/labex/project/sokoban

Le programme complet sample est distinct du moteur de l’apprenant. Exécutez-le, saisissez d, d, puis u, chacun suivi d’Enter, et observez le retour de l’état de réussite, du joueur et de la caisse à leurs valeurs avant la poussée gagnante. Saisissez q pour quitter.

./sample

Votre engine.cpp contient déjà le mouvement et le calcul de réussite corrects, mais n’enregistre pas l’historique et son undo() refuse toujours. Examinez-le :

cat engine.cpp

La classe préparée Game est déclarée dans engine.hpp. Point contient les coordonnées entières x et y : x augmente vers la droite et y vers le bas. State contient le joueur et un std::vector<Point> comprenant toutes les caisses. terrain[y][x] ne contient que des murs #, des espaces et des cibles .. const rend ce terrain accessible en lecture seule après sa construction : une caisse mobile ne peut donc pas effacer une cible. Le rendu combine ces deux couches : * représente une caisse sur une cible et +, le joueur sur une cible.

floor(point) indique si une coordonnée se trouve dans le plateau et n’est pas un mur. boxAt(point) renvoie l’indice d’une caisse dans le vecteur, ou -1 si aucune caisse n’occupe la case. Ces fonctions auxiliaires, le chargement, le rendu et la lecture des commandes sont préparés. Vous implémentez move, won, puis undo dans engine.cpp ; conservez engine.hpp, support.cpp, main.cpp, levels/ et sample. Vous pouvez ajouter des fonctions auxiliaires et utiliser des algorithmes équivalents. Les contrôles évaluent le comportement, pas une écriture ou un parcours particulier.

Les cartes acceptées sont des grilles ASCII rectangulaires fermées d’au moins trois lignes et trois colonnes. Elles contiennent exactement un joueur, au moins une caisse et autant de cibles que de caisses, sans chevauchement d’objets. Les symboles sont # pour un mur, un espace pour le sol, . pour une cible, @ pour le joueur, $ pour une caisse, + pour le joueur sur une cible et * pour une caisse sur une cible. Une carte peut être insoluble ou déjà terminée au départ. Le chargeur préparé refuse les autres entrées ; vous n’avez pas à écrire d’analyseur.

move(dx, dy) n’accepte que (1,0), (-1,0), (0,1) ou (0,-1). Toute autre paire d’entiers doit renvoyer false sans changer l’état ni l’historique. Une action autorisée renvoie true ; une action interdite renvoie false et ne modifie rien. L’ordre des caisses dans le vecteur n’importe pas pour l’occupation des cases. Plus tard, l’annulation restaurera l’état complet réellement enregistré avant cette action, y compris l’ordre des caisses à cet instant.

std::vector<State> history est une collection ordonnée d’états complets. history.push_back(state) enregistre une copie par valeur : modifier ensuite state.player ou un élément de state.boxes ne change pas cette copie antérieure. Ne sauvegarder que le joueur ferait perdre les positions des caisses poussées. Le terrain fixe n’est pas copié, car une partie autorisée ne peut pas le modifier ; la réussite est déduite du terrain et des caisses, plutôt que stockée dans un indicateur indépendant.

Une action refusée ne doit pas occuper une place d’annulation. Placez l’instantané après toutes les branches de refus, mais avant la première modification. Aucun instantané n’est utile lorsqu’une direction, un mur, une deuxième caisse ou une destination bloquée refuse le déplacement. L’historique initial est vide.

Le contrôle suivant échoue volontairement sur le code de départ avec walk records its entire before state :

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

Le fichier complet ci-dessous ajoute un instantané par action autorisée ; l’annulation reste inachevée. cat > remplace le fichier ; le document intégré entre guillemets écrit le C++ littéralement jusqu’à la ligne finale CPP.

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;
    history.push_back(state);
    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

Compilez avec C++17 et les avertissements du compilateur. Si une ligne est mal saisie, corrigez-la ou répétez le remplacement complet, puis reconstruisez.

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

Saisissez d, d, d, avec Enter après chaque commande. L’historique passe par 1, 2, 3 : marcher, pousser sur la cible, puis hors de la cible. Un autre d affiche Blocked et conserve History: 3. Saisissez u : No earlier move est attendu jusqu’à l’implémentation de l’annulation à l’étape suivante, et le plateau reste inchangé. Saisissez q.

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

All history checks passed signifie que les actions autorisées ont enregistré leurs valeurs antérieures réelles et complètes, tandis que les refus n’ont ajouté aucune entrée. Cela ne signifie pas encore que l’annulation fonctionne.

Restaurer et retirer le dernier instantané

Dans cette étape, implémentez l’annulation et jouez en revenant plusieurs fois en arrière. Continuez dans Terminal, dans /home/labex/project/sokoban.

history.empty() détecte l’absence d’action autorisée antérieure. Dans ce cas, renvoyez false sans rien modifier. Sinon, history.back() désigne le dernier instantané. L’affecter à state restaure le joueur et toutes les caisses par copie de valeur. Seulement après la copie, pop_back() supprime cet instantané utilisé. Accéder à back() sur un vecteur vide est invalide : la garde doit donc venir en premier. L’annulation n’ajoute pas d’autre entrée dans l’historique.

Le reste du mouvement ne change pas. Remplacez le fichier entier :

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;
    history.push_back(state);
    if (box >= 0) state.boxes[box] = beyond;
    state.player = next;
    return true;
}
bool Game::undo() {
    if (history.empty()) return false;
    state = history.back();
    history.pop_back();
    return true;
}
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

Saisissez u : No earlier move laisse le plateau initial inchangé. Saisissez d, d : la partie est gagnée. Saisissez encore d : la caisse quitte sa cible, qui apparaît comme + sous le joueur, et la victoire disparaît. Essayez un autre d : il est bloqué et l’historique reste à 3.

Saisissez maintenant u : la partie retrouve le plateau gagnant, y compris sa caisse. Un deuxième u restaure la position avant la poussée gagnante, avec Not solved. Un troisième u remet le joueur à sa position initiale avec History: 0 ; un autre u affiche sans risque No earlier move. Saisissez q.

./sokoban levels/warehouse.txt

Essayez w, s, d, d, w, avec Enter après chaque commande. Les deux caisses occupent leurs cibles. Saisissez u cinq fois : chaque étape restaure l’état précédent complet jusqu’à remettre les deux caisses et le joueur à leurs positions initiales. Une annulation supplémentaire est refusée sans risque. Un autre parcours autorisé est d, d, w, s, a, a, w ; le moteur doit accepter les deux parcours plutôt qu’une séquence fixe. Saisissez q.

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

All undo checks passed inclut la restauration des véritables états antérieurs après différents parcours, l’historique vide, sa préservation après une action interdite, la sortie d’une cible et l’inversion de la victoire. Le contrôleur utilise des parties isolées avec vos fonctions originales et votre exécutable sokoban déjà construit ; reconstruisez après modification pour que le programme jouable corresponde au source. Il n’envoie jamais de déplacements à une session en cours et ne réécrit pas votre progression.

Pour corriger une erreur ordinaire, une faute comme ./sokoban levels/missing.txt affiche Cannot open level: ... puis quitte. Répétez la commande avec levels/warehouse.txt. Une commande mal saisie dans le jeu en cours affiche Use one command per line... sans changer l’état ; saisissez une commande valide d’une lettre. Vous pouvez toujours saisir q, corriger le source si nécessaire, reconstruire et démarrer une nouvelle partie en mémoire sans modifier les niveaux originaux.

Résumé

Vous avez enregistré des instantanés par valeur avant les modifications autorisées, restauré chaque objet mobile et recalculé la victoire après annulation. Les actions interdites ne changent ni l’état du jeu ni l’historique.