Make Each Move Respect the Board

C++Beginner
Practice Now

Introduction

Build legal walking and single-box pushing in a playable C++ terminal warehouse game.

You will work on a small terminal game with fixed terrain and separate moving objects. You will implement the core C++ functions, then play the result and check its behavior.

Walk Without Crossing Walls

In this step, play a working example, then make your own player's movement respect the board. Start in the Terminal tab. If using the WebIDE editor, open its Terminal > New Terminal; all commands below run in that shell. cd changes this shell's working directory; it does not carry between new terminal sessions.

cd /home/labex/project/sokoban

The prepared sample is a complete demonstration, separate from your incomplete program. ./ runs a program from the current directory.

./sample

Enter d, press Enter, then enter d and press Enter again. These two moves walk toward the box and push it onto the goal. Solved! appears, and $ becomes *. Enter u and press Enter: both player and box return to their previous squares, and the message becomes Not solved. Enter q and press Enter to return to the shell. Commands are one letter per line; empty or unknown commands show help and change nothing. The reference demonstrates your destination; running it is not completion of your own engine.

The prepared Game class is declared in engine.hpp. Point holds integer x and y coordinates: x grows right and y grows down. State holds the player and a std::vector<Point> containing every box. terrain[y][x] contains only # walls, spaces, and . goals. const makes this terrain read-only after construction, so a moving box cannot erase a goal. Rendering combines the two layers: * means a box on a goal; + means the player on a goal.

floor(point) reports whether a coordinate is inside the board and not a wall. boxAt(point) returns a box's vector index, or -1 when no box occupies it. These helpers, loading, rendering, and command reading are prepared. You implement move, won, and later undo in engine.cpp; preserve engine.hpp, support.cpp, main.cpp, levels/, and sample. You may add helper functions and use equivalent algorithms. A check tests behavior, not a particular spelling or route.

Read the prepared declaration and your starter:

cat engine.hpp
cat engine.cpp

The starter refuses every move. This intentional self-check therefore prints FAIL walk changes only the player and exits nonzero; that means there is work left, not a broken environment:

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

Supported maps are enclosed rectangular ASCII grids at least three rows and columns wide. They contain exactly one player, at least one box, and the same number of goals as boxes, with no overlapping objects. Symbols are # wall, space floor, . goal, @ player, $ box, + player on goal, and * box on goal. A map can be unsolvable or already completed. The prepared loader rejects other input; you do not need to write a parser.

move(dx, dy) accepts only (1,0), (-1,0), (0,1), or (0,-1). Any other integer pair must return false without changing state or history. A legal action returns true; an illegal action returns false and changes nothing. Box vector order is not significant for occupancy. Later, undo restores the actual complete state saved before that action, including its then-current box order.

Start with walking only. A Point{...} constructs a coordinate value. && means both conditions must hold; || means either may hold. We explicitly list four directions before adding coordinates, which also safely rejects unusually large integers. >= 0 identifies an occupied box square. At this stage, boxes block walking; pushing comes next. The return false branches leave all state untouched.

To write the complete file from the terminal, cat > engine.cpp redirects text into that file, replacing its old contents. The quoted here-document marker <<'CPP' makes the shell take the following lines literally until a line containing only CPP. Copy the entire command, including its closing marker:

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

Compile the three source files. -std=c++17 selects C++17, -Wall -Wextra enables useful warnings, and -o sokoban names the executable. Compilation errors include a line number; correct that line in engine.cpp (or repeat the complete replacement) and run the same build again.

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

Enter a then Enter: the left wall gives Blocked, and the board stays unchanged. Enter d then Enter: the player walks right. A second d is still blocked by the box, which is expected in this step. u reports No earlier move: undo has not been implemented. Enter q to leave.

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

All walk checks passed confirms four-direction walking and safe refusals. The checks compile your actual engine in a temporary directory and also run your existing sokoban executable in isolated game instances. Rebuild sokoban after editing; a missing or stale executable is incomplete. Checks do not move your live game or change your source.

Push One Box and Keep the Goal

In this step, extend your engine to push one box and derive completion from its actual position. Continue in Terminal, in /home/labex/project/sokoban. Each run starts a fresh game in memory; editing or rebuilding the program does not save a live game.

Before pushing, check the square beyond the box. It must be floor or goal with no other box. Reject first, then change both the box and player together. This prevents pushing through walls or pushing a chain of two boxes. The box's index is an int because -1 means absent; only use it to index the vector after checking box >= 0.

won() const observes the game without changing it. The range loop for (Point box : state.boxes) visits every box coordinate by value. Every box must be on a . in fixed terrain; one completed box in a two-box map is insufficient. An empty box vector is never a completed game under our contract. Victory is calculated each time, so it becomes false again when a box leaves its goal. undo still deliberately refuses in this lab; history is the next lab's new responsibility.

Replace engine.cpp with the complete movement and completion implementation:

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

Enter d, d, each followed by Enter. The board shows * and Solved!. Enter another d: the box moves off the goal and the player stands on it as +; the game returns to Not solved. A further d is blocked because the box would enter the wall. The fixed goal still exists under the player. Enter q.

Try the prepared two-box warehouse by passing its file path as the program's optional argument:

./sokoban levels/warehouse.txt

One possible route is w, s, d, d, w, each followed by Enter. You can choose another legal route; no shortest solution is required. Both boxes must show * before Solved! appears. Enter q.

A misspelled level path produces Cannot open level: ... and exits without changing any level file. To see this ordinary error intentionally, run:

./sokoban levels/missing.txt

Recover by repeating the earlier command with levels/warehouse.txt. This is a filename correction, not a reason to reset the environment. Replaying a level starts a new in-memory game.

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

All move checks passed covers legal walks and pushes, invalid actions, goal preservation, an already completed map, and different two-box solutions. Keep the engine and prepared files; you now have a playable movement system.

Summary

You separated fixed terrain from moving objects, refused invalid actions without side effects, and completed two small warehouses. The next independent lab adds history and undo.