Haz que cada movimiento respete el tablero

C++Beginner
Practicar Ahora

Introducción

Implementa pasos legales y empujes de una sola caja en un juego de almacén para la terminal con 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.

Caminar sin atravesar paredes

En este paso, juega con un ejemplo completo y después haz que el movimiento de tu jugador respete el tablero. Empieza en la pestaña Terminal. Si usas el editor WebIDE, abre Terminal > New Terminal; todos los comandos siguientes se ejecutan en esa shell. cd cambia el directorio de trabajo de esa shell; el cambio no se transfiere a nuevas sesiones de terminal.

cd /home/labex/project/sokoban

El programa preparado sample es una demostración completa, separada de tu programa incompleto. ./ ejecuta un programa del directorio actual.

./sample

Introduce d, pulsa Enter, vuelve a introducir d y pulsa Enter. Estos dos movimientos acercan al jugador a la caja y la empujan al objetivo. Aparece Solved! y $ pasa a ser *. Introduce u y pulsa Enter: jugador y caja recuperan sus casillas anteriores y el mensaje pasa a Not solved. Introduce q y pulsa Enter para volver a la shell. Cada línea admite una sola letra; las órdenes vacías o desconocidas muestran ayuda sin cambiar nada. El ejemplo muestra el resultado esperado; ejecutarlo no completa tu propio motor.

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.

Lee la declaración preparada y tu código inicial:

cat engine.hpp
cat engine.cpp

El programa inicial rechaza cualquier movimiento. Por eso esta autocomprobación intencional muestra FAIL walk changes only the player y termina con un estado distinto de cero; indica trabajo pendiente, no un entorno averiado:

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

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.

Empieza solo con caminar. Point{...} construye un valor de coordenadas. && exige ambas condiciones; || exige al menos una. Enumeramos explícitamente las cuatro direcciones antes de sumar coordenadas, lo que también rechaza de forma segura enteros excepcionalmente grandes. >= 0 identifica una casilla ocupada por una caja. En esta fase, las cajas bloquean el paso; el empuje viene después. Las ramas return false no modifican ningún estado.

Para escribir el archivo completo desde la terminal, cat > engine.cpp redirige el texto a ese archivo y sustituye su contenido anterior. El marcador de documento incrustado entre comillas <<'CPP' hace que la shell lea literalmente las líneas siguientes hasta una línea que contenga solo CPP. Copia el comando entero, incluido su marcador de cierre:

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

Compila los tres archivos fuente. -std=c++17 selecciona C++17, -Wall -Wextra activa advertencias útiles y -o sokoban da nombre al ejecutable. Los errores de compilación incluyen un número de línea; corrige esa línea en engine.cpp (o repite la sustitución completa) y vuelve a ejecutar la misma compilación.

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

Introduce a y pulsa Enter: la pared izquierda provoca Blocked y el tablero no cambia. Introduce d y pulsa Enter: el jugador camina a la derecha. Un segundo d sigue bloqueado por la caja, como se espera en este paso. u muestra No earlier move: deshacer todavía no está implementado. Introduce q para salir.

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

All walk checks passed confirma el movimiento en cuatro direcciones y los rechazos seguros. Las comprobaciones compilan tu motor real en un directorio temporal y también ejecutan el binario sokoban existente en partidas aisladas. Reconstruye sokoban tras editar; un ejecutable ausente o desactualizado es una entrega incompleta. Las comprobaciones no mueven tu partida en curso ni cambian el código fuente.

Empujar una caja y conservar el objetivo

En este paso, amplía el motor para empujar una caja y determinar la finalización a partir de su posición real. Continúa en Terminal, dentro de /home/labex/project/sokoban. Cada ejecución inicia una partida nueva en memoria; editar o reconstruir el programa no guarda una partida en curso.

Antes de empujar, comprueba la casilla al otro lado de la caja. Debe ser suelo u objetivo sin otra caja. Primero rechaza lo inválido y después actualiza conjuntamente caja y jugador. Así no se empuja a través de paredes ni una cadena de dos cajas. El índice de la caja es un int porque -1 significa ausencia; úsalo para acceder al vector solo después de comprobar box >= 0.

won() const observa la partida sin modificarla. El bucle de rango for (Point box : state.boxes) recorre por valor las coordenadas de todas las cajas. Cada caja debe estar sobre un . del terreno fijo; una sola caja colocada en un mapa de dos cajas no basta. Según nuestro contrato, un vector de cajas vacío nunca constituye una partida completada. La victoria se calcula cada vez, por lo que vuelve a ser falsa cuando una caja abandona su objetivo. En este laboratorio undo todavía rechaza deliberadamente; el historial es la nueva responsabilidad del siguiente.

Sustituye engine.cpp por la implementación completa del movimiento y la finalización:

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

Introduce d, d, pulsando Enter después de cada uno. El tablero muestra * y Solved!. Introduce otro d: la caja sale del objetivo y el jugador queda sobre él como +; la partida vuelve a Not solved. Otro d se bloquea porque la caja entraría en la pared. El objetivo fijo sigue debajo del jugador. Introduce q.

Prueba el almacén preparado de dos cajas pasando su ruta de archivo como argumento opcional del programa:

./sokoban levels/warehouse.txt

Una ruta posible es w, s, d, d, w, pulsando Enter tras cada orden. Puedes elegir otra ruta legal; no se exige la solución más corta. Ambas cajas deben mostrar * antes de que aparezca Solved!. Introduce q.

Una ruta de nivel mal escrita produce Cannot open level: ... y termina sin modificar ningún archivo de nivel. Para observar intencionalmente este error habitual, ejecuta:

./sokoban levels/missing.txt

Recupérate repitiendo el comando anterior con levels/warehouse.txt. Solo debes corregir el nombre del archivo, no restablecer el entorno. Volver a abrir un nivel inicia una partida nueva en memoria.

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

All move checks passed cubre pasos y empujes legales, acciones inválidas, conservación de objetivos, un mapa ya completado y distintas soluciones de dos cajas. Conserva el motor y los archivos preparados; ya tienes un sistema de movimiento con el que puedes jugar.

Resumen

Separaste el terreno fijo de los objetos móviles, rechazaste acciones inválidas sin efectos secundarios y completaste dos almacenes pequeños. El siguiente laboratorio independiente añade historial y deshacer.