9-4.소코반

9-4-가.게임 소개

여기까지 여러분들은 변수, 루프, 연산자 등의 간단한 개념부터 시작해서 함수와 배열같은 조금 복잡한 과목까지 공부를 마쳤다. 물론 아직도 갈 길이 멀기는 하지만 여기까지 역경을 헤쳐온 것만 해도 작지 않은 성과다. 설사 앞 부분의 내용을 100% 다 이해하지 못했다 하더라도 지금 이 글을 읽고 있다는 것 자체가 기본적인 코딩 준비가 완료되었다고 평가할 수 있다. 적어도 프로그래밍이 뭔지는 안다고 해도 좋을 정도가 된 것이다.

이 단계에 이르면 지금까지 배웠던 이론들을 위한 종합 실습이 필요하다. 개별적으로 학습했던 이론들이 실제 코딩에서 어떻게 적용되고 또 서로 어떤 식으로 결합되는지를 확인할 수 있을 것이다. 도대체 이런 건 어디다 쓰나 싶었던 것들이 실전에서 요긴하게 사용되는 예를 볼 수 있을 것이며 무심코 사소하게 넘겼던 것들에게도 관심을 가지게 될 것이다. 이 실습을 통해 나무가 아닌 숲(비록 작은 동산 정도에 불과하지만)을 구경해 보고 앞으로의 학습 방향 설정에 도움이 되기 바란다.

초보자를 위한 종합 예제로는 역시 게임이 가장 좋은데 규칙이 간단하고 결과를 화면으로 즉시 즉시 확인해 볼 수 있기 때문이다. 여기서는 아주 간단한 게임을 하나 소개하는데 화면은 썰렁하기 그지 없고 논리도 지극히 간단하다. 하지만 이 게임은 지금까지 문법 공부를 위해 작성했던 짧은 예제들과는 질적으로 다르다. 작고 초라하지만 독립된 기능을 수행하는 완결된 프로그램이므로 만만하게만 볼 수는 없을 것이다.

이 게임은 학습을 위한 예제이므로 의도적으로 지금까지 배웠던 것들만 사용했다. 포인터, 문자열, 구조체, 클래스같이 어려운 것은 사용하지 않았으므로 이 책을 제대로 읽고 있다면 누구나 100% 이해할 수 있을 것이다. 따라서 이 예제를 통해 1~9장까지 총정리를 할 수 있으며 만약 모르는 부분이 있다면 다시 앞장으로 돌아가서 한 번 더 정리해 보기 바란다. 만약 이 단계에서 확실하게 정리를 하지 않고 넘어가게 되면 다음 장의 포인터에서부터 엄청난 혼란을 겪을 것이다.

소코반은 전형적인 퍼즐 게임으로서 규칙이 간단해서 누구나 쉽게 즐길 수 있는 게임이다. 1980년대 초반에 일본에서 처음 개발되었으며 각종 플랫폼으로 이식되어 테트리스 이전에 상당한 인기를 누렸던 고전 게임이다. 인터넷의 공개 자료실을 찾아 보면 아주 많은 종류의 소코반을 찾을 수 있으며 일부는 웹에서 바로 게임을 즐길 수도 있다. 최근에는 핸드폰에도 푸시 푸시라는 이름으로 이식되어 지하철에서 이 게임을 하는 예쁜 소녀들을 볼 수 있다.

소스가 공개되어 있는 것들도 많이 있으므로 이 정도 게임은 혼자서도 얼마든지 만들 수 있을 것이다. 다음 프로그램이 오리지날 소코반 게임의 예인데 오른쪽은 웹 페이지에서 실행할 수 있는 자바 버전이다.

 

커서 이동키로 사람을 움직여 바깥에 쌓여 있는 짐들을 창고의 지정한 자리로 이동시키되 짐을 밀수만 있으며 당길 수는 없다. 그리고 동시에 두 개 이상의 짐을 밀 수 없도록 되어 있어 순서를 잘 생각해가며 짐을 이동시켜야 게임을 풀 수 있다. 짐이 벽 모서리에 닿는다거나 두 개 이상의 짐이 벽면에 붙어 버리면 더 이상 이동 불가능한 상태가 되므로 이 상태가 되지 않도록 조심해야 한다.

자료실을 찾아 보면 원형 소코반에 그래픽과 사운드를 강화하고 다양한 변형 맵을 제공하는 아류작들도 많이 발표되어 있으며 약간 다른 규칙이 제공되는 것들도 있다. 간단한 아이디어에서 출발한 게임이지만 이런 게임도 애니메이션과 사운드가 추가되면 그럴싸해 보이고 재미가 배가되기 마련이다. 일단 콘솔 모드에서 실행되는 소코반 게임을 만든 후 차후에 그래픽 버전으로 옮겨 보도록 하자. 전체 소스는 다음과 같다.

 

: Sokoban

#include <Turboc.h>

 

#define LEFT 75

#define RIGHT 77

#define UP 72

#define DOWN 80

#define MAXSTAGE 3

#define putchxy(x,y,c) {gotoxy(x,y);putch(c);}

 

void DrawScreen();

BOOL TestEnd();

void Move(int dir);

 

char ns[18][21];

int nStage;

int nx,ny;

int nMove;

 

char arStage[MAXSTAGE][18][21]={

     {

     "####################",

     "####################",

     "####################",

     "#####   ############",

     "#####O  ############",

     "#####  O############",

     "###  O O ###########",

     "### # ## ###########",

     "#   # ## #####  ..##",

     "# O  O   @      ..##",

     "##### ### # ##  ..##",

     "#####     ##########",

     "####################",

     "####################",

     "####################",

     "####################",

     "####################",

     "####################"

     },

     {

     "####################",

     "####################",

     "####################",

     "####################",

     "####..  #     ######",

     "####..  # O  O  ####",

     "####..  #O####  ####",

     "####..    @ ##  ####",

     "####..  # #  O #####",

     "######### ##O O ####",

     "###### O  O O O ####",

     "######    #     ####",

     "####################",

     "####################",

     "####################",

     "####################",

     "####################",

     "####################"

     },

     {

     "####################",

     "####################",

     "####################",

     "####################",

     "##########     @####",

     "########## O#O #####",

     "########## O  O#####",

     "###########O O #####",

     "########## O # #####",

     "##....  ## O  O  ###",

     "###...    O  O   ###",

     "##....  ############",

     "####################",

     "####################",

     "####################",

     "####################",

     "####################",

     "####################"

     },

};

 

void main()

{

     int ch;

     int x,y;

 

     setcursortype(NOCURSOR);

     nStage=0;

 

     for (;1;) {

          memcpy(ns,arStage[nStage],sizeof(ns));

          for (y=0;y<18;y++) {

              for (x=0;x<20;x++) {

                   if (ns[y][x]=='@') {

                        nx=x;

                        ny=y;

                        ns[y][x]=' ';

                   }

              }

          }

          clrscr();

          nMove=0;

 

          for (;2;) {

              DrawScreen();

              ch=getch();

              if (ch==0xE0 || ch==0) {

                   ch=getch();

                   switch (ch) {

                   case LEFT:

                   case RIGHT:

                   case UP:

                   case DOWN:

                        Move(ch);

                        break;

                   }

              } else {

                   ch=tolower(ch);

                   if (ch=='r') {

                        break;

                   }

                   if (ch=='n') {

                        if (nStage < MAXSTAGE-1) {

                             nStage++;

                        }

                        break;

                   }

                   if (ch=='p') {

                        if (nStage > 0) {

                             nStage--;

                        }

                        break;

                   }

                   if (ch=='q') {

                        setcursortype(NORMALCURSOR);

                        exit(0);

                   }

              }

 

              if (TestEnd()) {

                   clrscr();

                   gotoxy(10,10);

                   printf("%d 스테이지를 풀었습니다. 다음 스테이지로 이동합니다",

                        nStage+1);

                   delay(2000);

                   if (nStage < MAXSTAGE-1) {

                        nStage++;

                   }

                   break;

              }

          }

     }

}

 

void DrawScreen()

{

     int x,y;

     for (y=0;y<18;y++) {

          for (x=0;x<20;x++) {

              putchxy(x,y,ns[y][x]);

          }

     }

     putchxy(nx,ny,'@');

 

     gotoxy(40,2);puts("SOKOBAN");

     gotoxy(40,4);puts("Q:종료, R:다시 시작");

     gotoxy(40,6);puts("N:다음, P:이전");

     gotoxy(40,8);printf("스테이지 : %d",nStage+1);

     gotoxy(40,10);printf("이동 회수 : %d",nMove);

}

 

BOOL TestEnd()

{

     int x,y;

 

     for (y=0;y<18;y++) {

          for (x=0;x<20;x++) {

              if (arStage[nStage][y][x]=='.' && ns[y][x]!='O') {

                   return FALSE;

              }

          }

     }

     return TRUE;

}

 

void Move(int dir)

{

     int dx=0,dy=0;

 

     switch (dir) {

     case LEFT:

          dx=-1;

          break;

     case RIGHT:

          dx=1;

          break;

     case UP:

          dy=-1;

          break;

     case DOWN:

          dy=1;

          break;

     }

 

     if (ns[ny+dy][nx+dx]!='#') {

          if (ns[ny+dy][nx+dx]=='O') {

              if (ns[ny+dy*2][nx+dx*2] == ' ' || ns[ny+dy*2][nx+dx*2] == '.') {

                   if (arStage[nStage][ny+dy][nx+dx]=='.') {

                        ns[ny+dy][nx+dx]='.';

                   } else {

                        ns[ny+dy][nx+dx]=' ';

                   }

                   ns[ny+dy*2][nx+dx*2]='O';

              } else {

                   return;

              }

          }

          nx+=dx;

          ny+=dy;

          nMove++;

     }

}

 

전체 소스의 줄 수는 200줄이 조금 더 되지만 그 중 절반이 스테이지 데이터와 매크로 정의 등이므로 실제로 게임의 논리를 다루는 줄은 십여줄도 채 안되는 편이다. main과 세 개의 함수로만 구성되어 있으며 전역변수도 몇 개 되지 않는 아주 간단한 게임이라 어렵지 않게 분석할 수 있을 것이다.

물론 남이 작성한 소스를 분석한다는 것은 쉽지 않은 일이다. 주석도 없고, 변수 이름도 엉망이고 구조도 안좋고 게다가 다량의 꽁수까지 곁들여지고 양까지 많으면 차라리 처음부터 다시 만드는 게 더 쉬울 수도 있다. 그래도 소코반 예제는 짧은데다 교육용으로 만들어져 있기 때문에 아주 간결하고 쉬운편에 속한다. 실무에서는 동료의 소스를 봐야 한다거나 주요 기술 습득을 위해 인터넷에서 구한 소스를 분석한다거나 인수 인계를 위해 전임자의 소스를 원치않게 이어서 짜야 하는 경우도 있다. 남의 코드를 잘 분석하는 것도 일종의 기술이며 고급 기술을 흡수하기 위한 수단이다. 싫어도 자꾸 보는 습관을 가져야 한다.