Restaura todo el estado anterior

C++Beginner
Practicar Ahora

Introducción

Añade instantáneas completas por valor y deshacer seguro de varios pasos a un juego Sokoban en C++.

Trabajarás en un pequeño juego de terminal con terreno fijo y objetos móviles separados. Implementarás las funciones centrales de C++ y después jugarás para comprobar su comportamiento.

En este paso, registra estados anteriores completos para que después puedas deshacer tanto pasos como empujes. Este laboratorio inicializa sus propios archivos y no necesita los del anterior. Empieza en la pestaña Terminal o usa Terminal > New Terminal dentro de WebIDE.

cd /home/labex/project/sokoban

El programa completo sample está separado del motor del estudiante. Ejecútalo, introduce d, d y después u, pulsando Enter tras cada orden, y observa cómo el estado de finalización, el jugador y la caja vuelven a los valores anteriores al empuje ganador. Introduce q para salir.

./sample

Tu engine.cpp ya contiene movimiento y finalización correctos, pero no registra historial y su undo() siempre rechaza. Examínalo:

cat engine.cpp

La clase preparada Game se declara en engine.hpp. Point contiene las coordenadas enteras x e y: x aumenta hacia la derecha e y hacia abajo. State contiene al jugador y un std::vector<Point> con todas las cajas. terrain[y][x] solo contiene paredes #, espacios y objetivos .. const hace que el terreno sea de solo lectura tras su construcción, por lo que una caja móvil no puede borrar un objetivo. La representación combina las dos capas: * indica una caja sobre un objetivo y +, al jugador sobre un objetivo.

floor(point) indica si una coordenada está dentro del tablero y no es una pared. boxAt(point) devuelve el índice de una caja en el vector, o -1 si no hay ninguna allí. Estos auxiliares, la carga, la representación y la lectura de órdenes ya están preparados. Implementas move, won y, más adelante, undo en engine.cpp; conserva engine.hpp, support.cpp, main.cpp, levels/ y sample. Puedes añadir auxiliares y usar algoritmos equivalentes. La comprobación evalúa el comportamiento, no una escritura o ruta concreta.

Los mapas admitidos son cuadrículas ASCII rectangulares cerradas, de al menos tres filas y tres columnas. Contienen exactamente un jugador, al menos una caja y tantos objetivos como cajas, sin objetos superpuestos. Los símbolos son # pared, espacio suelo, . objetivo, @ jugador, $ caja, + jugador sobre objetivo y * caja sobre objetivo. Un mapa puede no tener solución o estar completado desde el principio. El cargador preparado rechaza otras entradas; no necesitas escribir un analizador.

move(dx, dy) solo acepta (1,0), (-1,0), (0,1) o (0,-1). Cualquier otro par de enteros debe devolver false sin cambiar el estado ni el historial. Una acción legal devuelve true; una ilegal devuelve false y no cambia nada. El orden del vector de cajas no importa para la ocupación. Más adelante, deshacer restaura el estado completo real guardado antes de esa acción, incluido el orden de las cajas en aquel momento.

std::vector<State> history es una colección ordenada de estados completos. history.push_back(state) guarda una copia por valor: los cambios posteriores en state.player o en cualquier elemento de state.boxes no modifican esa copia anterior. Guardar solo al jugador perdería las posiciones de las cajas empujadas. El terreno fijo no se copia porque una partida legal no puede cambiarlo; la finalización se deriva del terreno y las cajas en lugar de guardarse como un indicador independiente.

Una acción rechazada no debe ocupar un lugar en deshacer. Guarda la instantánea después de todas las ramas de rechazo, pero antes de la primera modificación. No hace falta una instantánea cuando una dirección, pared, segunda caja o destino bloqueado rechaza el movimiento. El historial inicial está vacío.

La siguiente comprobación falla intencionalmente con el código inicial y muestra walk records its entire before state:

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

El archivo completo siguiente añade una instantánea por acción legal; deshacer sigue sin terminar. cat > sustituye el archivo; el documento incrustado entre comillas escribe literalmente C++ hasta la línea de cierre 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

Compila con C++17 y advertencias del compilador. Si hay una línea mal escrita, corrígela o repite la sustitución completa y vuelve a compilar.

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

Introduce d, d, d, pulsando Enter tras cada uno. El historial pasa por 1, 2, 3: caminar, empujar al objetivo y empujar fuera de él. Otro d muestra Blocked y conserva History: 3. Introduce u: se espera No earlier move hasta implementar deshacer en el siguiente paso y el tablero permanece igual. Introduce q.

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

All history checks passed significa que las acciones legales guardaron sus valores anteriores reales y completos, mientras que los rechazos no añadieron entradas. Todavía no significa que deshacer funcione.

Restaurar y retirar la última instantánea

En este paso, implementa deshacer y juega retrocediendo varias veces. Continúa en Terminal, dentro de /home/labex/project/sokoban.

history.empty() detecta que no existe una acción legal anterior. En ese caso devuelve false sin modificar nada. En caso contrario, history.back() se refiere a la última instantánea. Asignarla a state restaura al jugador y todas las cajas mediante copia por valor. Solo después de copiar, pop_back() elimina la instantánea utilizada. Acceder a back() en un vector vacío no es válido; por eso la guarda va primero. Deshacer no añade otra entrada al historial.

El resto del movimiento sigue igual. Sustituye el archivo completo:

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

Introduce u: No earlier move deja el tablero inicial intacto. Introduce d, d: completas la partida. Introduce de nuevo d: la caja sale del objetivo, que aparece como + bajo el jugador, y la victoria desaparece. Prueba otro d: se bloquea y el historial sigue en 3.

Ahora introduce u: la partida vuelve al tablero ganador, incluida la caja. Un segundo u restaura la posición anterior al empuje ganador, con Not solved. Un tercer u vuelve a la posición inicial del jugador con History: 0; otro u muestra de forma segura No earlier move. Introduce q.

./sokoban levels/warehouse.txt

Prueba w, s, d, d, w, pulsando Enter tras cada orden. Las dos cajas llegan a sus objetivos. Introduce u cinco veces: cada paso restaura el estado anterior completo hasta devolver ambas cajas y al jugador a sus posiciones iniciales. Más intentos de deshacer se rechazan de forma segura. Otra ruta legal es d, d, w, s, a, a, w; el motor debe admitir ambas rutas, no una secuencia fija. Introduce q.

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

All undo checks passed incluye la restauración de los estados anteriores reales tras distintas rutas, historial vacío, conservación del historial ante acciones ilegales, salida de un objetivo y reversión de la victoria. El comprobador usa partidas aisladas con tus funciones originales y el ejecutable sokoban ya construido; recompila tras editar para que el programa jugable coincida con el código. Nunca envía movimientos a una sesión en curso ni reescribe tu progreso.

Para recuperarte de errores habituales, una errata como ./sokoban levels/missing.txt muestra Cannot open level: ... y termina. Repite el comando con levels/warehouse.txt. Una orden mal escrita dentro del juego en ejecución muestra Use one command per line... sin cambiar el estado; introduce una orden válida de una letra. Siempre puedes introducir q, corregir el código si hace falta, recompilar e iniciar una partida nueva en memoria sin cambiar los niveles originales.

Resumen

Guardaste instantáneas por valor antes de los cambios legales, restauraste todos los objetos móviles y volviste a determinar la victoria tras deshacer. Las acciones ilegales no cambian el estado ni el historial.