소개
C++ 터미널 창고 게임에 올바른 걷기와 상자 하나 밀기를 구현해 실제로 플레이합니다.
고정 지형과 움직이는 객체를 분리한 작은 터미널 게임을 다룹니다. 핵심 C++ 함수를 구현한 뒤 실제로 플레이하며 동작을 확인합니다.
벽을 통과하지 않고 걷기
이번 단계에서는 완성된 예제를 플레이한 다음, 자신의 플레이어가 보드 규칙에 맞게 움직이도록 만듭니다. Terminal 탭에서 시작하세요. WebIDE 편집기를 사용한다면 Terminal > New Terminal을 여세요. 아래 명령은 모두 해당 셸에서 실행합니다. cd는 이 셸의 작업 디렉터리를 바꾸며, 새 터미널 세션에 그 변경이 이어지지는 않습니다.
cd /home/labex/project/sokoban
준비된 sample은 아직 완성하지 않은 자신의 프로그램과 별개인 완성형 데모입니다. ./는 현재 디렉터리에 있는 프로그램을 실행합니다.
./sample
d를 입력하고 Enter를 누른 뒤, 다시 d를 입력하고 Enter를 누르세요. 두 번의 이동으로 상자에 다가가 상자를 목표 칸으로 밉니다. Solved!이 표시되고 $가 *로 바뀝니다. u를 입력하고 Enter를 누르면 플레이어와 상자가 모두 이전 칸으로 돌아가며 메시지는 Not solved가 됩니다. q를 입력하고 Enter를 누르면 셸로 돌아갑니다. 한 줄에 한 글자의 명령을 입력합니다. 빈 명령이나 알 수 없는 명령은 도움말을 표시하고 아무것도 바꾸지 않습니다. 이 예제는 완성할 모습을 보여 줄 뿐이며, 실행했다고 자신의 엔진이 완성되는 것은 아닙니다.
준비된 Game 클래스는 engine.hpp에 선언되어 있습니다. Point는 정수 좌표 x와 y를 보관합니다. x는 오른쪽으로, y는 아래쪽으로 증가합니다. State는 플레이어와 모든 상자를 담은 std::vector<Point>를 보관합니다. terrain[y][x]에는 벽 #, 공백, 목표 .만 있습니다. const는 생성 이후 지형을 읽기 전용으로 만들어 움직이는 상자가 목표를 지우지 못하게 합니다. 화면을 그릴 때 두 층을 합칩니다. *는 목표 위의 상자, +는 목표 위의 플레이어입니다.
floor(point)는 좌표가 보드 안에 있고 벽이 아닌지 알려 줍니다. boxAt(point)는 상자의 vector 인덱스를 반환하며, 해당 칸에 상자가 없으면 -1을 반환합니다. 이 보조 함수와 로딩, 화면 출력, 명령 읽기는 준비되어 있습니다. engine.cpp에서 move, won, 이후에 undo를 구현합니다. engine.hpp, support.cpp, main.cpp, levels/, sample은 보존하세요. 보조 함수를 추가하고 동등한 알고리즘을 사용해도 됩니다. 검사는 특정 표기나 이동 경로가 아니라 동작을 평가합니다.
준비된 선언과 시작 코드를 읽어 보세요.
cat engine.hpp
cat engine.cpp
시작 프로그램은 모든 이동을 거부합니다. 따라서 이 의도적인 자체 검사는 FAIL walk changes only the player를 출력하고 0이 아닌 상태로 종료합니다. 이는 구현할 일이 남았다는 뜻이지, 환경이 고장 났다는 뜻이 아닙니다.
bash /opt/labex-sokoban/check.sh walk
지원하는 맵은 벽으로 둘러싸인 직사각형 ASCII 격자이며, 최소 3행 3열입니다. 플레이어는 정확히 하나, 상자는 하나 이상이고 목표 수는 상자 수와 같으며 객체가 겹치지 않습니다. 기호는 벽 #, 바닥인 공백, 목표 ., 플레이어 @, 상자 $, 목표 위의 플레이어 +, 목표 위의 상자 *입니다. 풀 수 없는 맵이나 시작부터 완성된 맵도 허용됩니다. 준비된 로더는 다른 입력을 거부하므로 파서를 작성할 필요는 없습니다.
move(dx, dy)는 (1,0), (-1,0), (0,1), (0,-1)만 받습니다. 다른 모든 정수 쌍은 상태나 이력을 바꾸지 않고 false를 반환해야 합니다. 올바른 동작은 true를 반환하고, 허용되지 않는 동작은 false를 반환하며 아무것도 바꾸지 않습니다. 상자 vector의 순서는 칸의 점유 관계에는 중요하지 않습니다. 이후 실행 취소는 그 동작 전에 실제로 저장한 전체 상태를 당시 상자 순서까지 포함하여 복원합니다.
우선 걷기만 구현합니다. Point{...}는 좌표 값을 만듭니다. &&는 두 조건 모두를, ||는 어느 하나의 조건을 요구합니다. 좌표를 더하기 전에 네 방향을 명시적으로 나열하면 매우 큰 정수도 안전하게 거부할 수 있습니다. >= 0은 상자가 차지한 칸을 뜻합니다. 지금은 상자가 걷기를 막고, 밀기는 다음에 구현합니다. return false 분기는 모든 상태를 그대로 둡니다.
터미널에서 전체 파일을 작성할 때 cat > engine.cpp는 텍스트를 해당 파일로 리다이렉트하여 기존 내용을 바꿉니다. 따옴표로 감싼 here-document 표시 <<'CPP'는 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) || boxAt(next) >= 0) return false;
state.player = next;
return true;
}
bool Game::undo() { return false; }
bool Game::won() const { return false; }
CPP
소스 파일 세 개를 컴파일하세요. -std=c++17은 C++17을 선택하고, -Wall -Wextra는 유용한 경고를 켜며, -o sokoban은 실행 파일 이름을 정합니다. 컴파일 오류에는 줄 번호가 나옵니다. engine.cpp의 해당 줄을 고치거나 파일 전체 교체를 반복한 뒤 같은 빌드를 다시 실행하세요.
g++ -std=c++17 -Wall -Wextra engine.cpp support.cpp main.cpp -o sokoban
./sokoban
a를 입력하고 Enter를 누르면 왼쪽 벽 때문에 Blocked가 표시되고 보드는 변하지 않습니다. d를 입력하고 Enter를 누르면 플레이어가 오른쪽으로 걷습니다. 두 번째 d는 여전히 상자에 막히며, 이 단계에서는 예상한 동작입니다. u는 No earlier move를 표시합니다. 아직 실행 취소를 구현하지 않았기 때문입니다. q를 입력해 나가세요.
bash /opt/labex-sokoban/check.sh walk
All walk checks passed는 네 방향 걷기와 안전한 거부를 확인합니다. 검사는 임시 디렉터리에서 실제 엔진을 컴파일하고, 기존 sokoban 실행 파일도 격리된 게임에서 실행합니다. 편집 후에는 sokoban을 다시 빌드하세요. 실행 파일이 없거나 오래되었다면 아직 완성된 결과가 아닙니다. 검사는 플레이 중인 게임을 움직이거나 소스를 바꾸지 않습니다.
상자 하나를 밀고 목표 보존하기
이번 단계에서는 상자 하나를 밀고 실제 위치에 따라 완료 여부를 판단하도록 엔진을 확장합니다. Terminal의 /home/labex/project/sokoban에서 계속하세요. 실행할 때마다 메모리에 새 게임이 시작됩니다. 프로그램을 편집하거나 다시 빌드해도 현재 게임이 저장되지는 않습니다.
밀기 전에 상자 너머의 칸을 확인합니다. 다른 상자가 없는 바닥이나 목표여야 합니다. 먼저 거부할 조건을 판단하고 나서 상자와 플레이어를 함께 바꿉니다. 이렇게 해야 벽 너머로 밀거나 한 번의 동작으로 인접한 두 상자를 함께 밀지 않습니다. -1이 상자 없음을 나타내므로 상자 인덱스는 int입니다. box >= 0을 확인한 뒤에만 vector 인덱스로 사용하세요.
won() const는 게임을 바꾸지 않고 살펴봅니다. 범위 기반 반복문 for (Point box : state.boxes)는 각 상자 좌표를 값으로 가져옵니다. 모든 상자가 고정 지형의 .에 있어야 하므로 두 상자 맵에서 하나만 도착해서는 부족합니다. 이 계약에서 빈 상자 vector는 완성된 게임이 아닙니다. 승리 여부는 매번 계산하므로 상자가 목표를 떠나면 다시 거짓이 됩니다. 이번 실습의 undo는 아직 의도적으로 거부합니다. 이력은 다음 실습의 새 과제입니다.
engine.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;
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
d, d를 각각 Enter를 눌러 입력합니다. 보드에 *와 Solved!이 표시됩니다. d를 하나 더 입력하면 상자가 목표를 떠나고 플레이어가 그 위에 +로 서며, 게임은 Not solved로 돌아갑니다. 다시 d를 입력해도 상자가 벽에 들어가게 되므로 막힙니다. 고정 목표는 플레이어 아래에 그대로 있습니다. q를 입력하세요.
파일 경로를 프로그램의 선택적 인자로 넘겨 준비된 두 상자 창고를 플레이해 보세요.
./sokoban levels/warehouse.txt
가능한 경로 하나는 w, s, d, d, w이며, 매번 Enter를 누릅니다. 다른 올바른 경로를 선택해도 되고 최단 해법을 요구하지 않습니다. 두 상자가 모두 *가 되어야 Solved!이 나타납니다. q를 입력하세요.
레벨 경로를 잘못 쓰면 Cannot open level: ...를 출력하고, 레벨 파일을 바꾸지 않은 채 종료합니다. 이 일반적인 오류를 의도적으로 보려면 다음을 실행하세요.
./sokoban levels/missing.txt
앞의 명령을 levels/warehouse.txt로 다시 실행하면 복구됩니다. 환경을 초기화할 문제가 아니라 파일 이름을 바로잡는 것입니다. 레벨을 다시 실행하면 메모리에 새 게임이 시작됩니다.
bash /opt/labex-sokoban/check.sh move
All move checks passed는 올바른 걷기와 밀기, 잘못된 동작, 목표 보존, 처음부터 완성된 맵, 서로 다른 두 상자 해법을 확인합니다. 엔진과 준비된 파일을 보존하세요. 이제 실제로 플레이할 수 있는 이동 시스템이 있습니다.
요약
고정 지형과 움직이는 객체를 분리하고, 잘못된 동작을 부작용 없이 거부하며, 작은 창고 두 개를 완성했습니다. 다음 독립 실습에서는 이력과 실행 취소를 추가합니다.



