하노이 탑 쌓기 게임 만들기(상)

자바스크립트 하노이 탑 쌓기/하노이 타워 게임 스크린샷

<이 화면은 스크린샷 그림입니다>

하노이 탑 쌓기 혹은 하노이 타워라고도 불리는 이 게임은 베트남 하노이의 승려들이 했던 퍼즐게임입니다. 세 개의 장대와 크기가 다른 원판들로 구성되어 있으며 가장 왼쪽의 장대에 원판들이 크기순으로 정렬하여 가장 큰 것이 밑쪽에 가도록 쌓여 있습니다. 게임의 목표는 모든 원판을 가장 오른쪽 장대로 모두 이동하는 것이며 규칙은 다음과 같습니다.

  • 한번에 하나의 원판만 집어서 해당 원판을 이동할 수 있습니다.
  • 작은 원판 위에 그 원판보다 큰 원판을 쌓을 수 없습니다.

자바스크립트와 Text Game Maker JS 라이브러리를 사용하여 이 게임을 만들어 봅시다. 게임데이터를 화면에 출력하는 부분과 키입력을 받는 부분에만 해당 라이브러리가 사용되고 나머지 모든 코드들은 네이티브 자바스크립트로 작성합니다. Text Game Maker JS 코드가 사용된 부분들은 이 강의 본문에 간단하게만 설명되니 Text Game Maker JS를 더 자세히 알고 싶은 분들은 Text Game Maker JS 튜토리얼 강의를 읽어 주세요.

상편과 하편으로 나누어서 살펴 볼텐데, 이번 상편에서는 게임에 필요한 변수들과 화면에 텍스트들을 출력하는 함수들을 만들어 보겠습니다.

Text Game Maker JS Starter Program 설치

우선 Text Game Maker JS를 받아서 설치합니다. https://github.com/a-mean-blogger/text-game-maker-js/releases 에 이동하면 text-game-maker-js-starter-program.ziptext-game-maker-1.0.0.min.js.zip 두 개의 압축파일이 있는데, text-game-maker-js-starter-program.zip을 다운받아 적당한 폴더에 압축을 풀어 줍시다. 이 파일은 TM 라이브러리 파일과 간단한 데모 코드가 작성된 압축파일이고, text-game-maker-1.0.0.min.js.zip은 TM 라이브러리 파일만 들어있습니다.

text-game-maker-js-starter-program.zip의 압축을 풀고 index.html을 더블 클릭하면 웹 브라우저에 데모 프로그램이 실행됩니다. main.js의 코드를 변경하여 프로그램을 만들게 됩니다. 데모 프로그램이 문제없이 실행되는 것을 확인하면 main.js의 코드를 전부 지워서 새로운 코드를 작성할 준비를 합시다.

소스 코드

하노이 탑 쌓기 게임 만들기(상)의 소스 코드를 여기를 클릭하여 확인해 주세요. 전체 코드를 복사한 후 위에서 다운 받은 TM 스타터 프로그램의 main.js 파일에 붙여넣기 합니다. 그 다음 index.html을 더블 클릭하면 아래와 같이 웹 브라우저가 나타나게 됩니다.

자바스크립트 하노이 탑 쌓기/하노이 타워 게임 브라우저 스크린샷

main.js의 코드를 살펴봅시다.

var screenSetting = {
  // canvasId: 'tm-canvas',
  // frameSpeed: 40,
  // column: 60,
  // row: 20,
  // backgroundColor: '#151617',
  // webFontJsPath: 'https://ajax.googleapis.com/ajax/libs/webfont/1.6.26/webfont.js',
  // fontColor: '#F5F7FA',
  // fontFamily: 'monospace',
  // fontSource: null,
  // fontSize: 30,
  // zoom: 0.5,
  column: 56,
  row: 19,
  fontFamily: 'Consolas',
};

var debugSetting = {
  // devMode: false,
  // outputDomId: 'tm-debug-output',
  devMode: true,
};

screenSettingdebugSetting은 Text Game Maker JS를 사용하기 위해 설정값을 조절하는 부분입니다. 주석 처리된 부분은 해당 항목이 없을 경우 사용될 기본값을 나타냅니다. 각 항목의 의미는 TM.defaultSettings 문서에서 확인할 수 있습니다.

var TMS = new TM.ScreenManager(screenSetting),
    TMI = new TM.InputManager(screenSetting.canvasId,debugSetting.devMode),
    TMD = new TM.DebugManager(debugSetting);

screenSettingdebugSetting을 사용하여 게임 제작에 필요한 TM 메니저들을 생성합니다.

  • TMS: 화면 생성및 출력을 담당합니다.
  • TMI: 키입력을 담당합니다. 이번 포스트에는 사용되지 않고 다음 포스트에서 사용됩니다.
  • TMD: 디버그용 데이터 출력을 담당합니다.

각 인스턴스의 함수 및 자세한 설명은 TM.ScreenManager, TM.InputManager, TM.DebugManager 문서에서 확인할 수 있습니다.

var NUM_OF_DISKS = 4;
var NUM_OF_POLES = 3;
var COLUMN_WIDTH = NUM_OF_DISKS*2+1;
var COLUMN_GAP = 4;
var FRAME_X = 6;
var FRAME_Y = 1;
var DISK_COLORS = ['#999', '#BBB', '#DDD', '#FFF'];
var KEYSET = {
  LEFT: 37,
  RIGHT: 39,
  ENTER: 13,
  ESC: 27,
};
var DISK_CHAR = "#";
var CURSOR_CHAR = "↓";
var GROUND_CHAR = "─";
var POLE_CHAR = "|";

var towerData;
var moveCount;
var cursorPosition;
var cursorDiskValue;
var isGameOver;

하노이 타워 게임에서 사용될 상수와 변수들입니다. 상수는 프로그램이 실행되는 동안 변하지 않는 값이며 대문자와 _를 사용하여 나타내었습니다. 변수는 프로그램이 실행되는 동안 변할 수 있는 값이며 Camel Case(띄어쓰기 대신 대문자사용)로 나타내었습니다.

  • NUM_OF_DISKS: 원판의 수입니다. 원판들이 실제 화면에서 출력될 때의 칸수는 이 값에 따라 자동으로 생성되는데, 2n+1(n은 1부터 NUM_OF_DISKS까지) 칸의 원판이 생성됩니다. 현재 값은 4이므로 2n+1에서 n에 1부터 4까지 값이 들어간 3, 5, 7, 9칸 4개의 원판이 화면에 출력됩니다. 이 값을 조절하여 게임에서 사용할 원판의 수를 바꿀 수 있습니다.
  • NUM_OF_POLES: 장대의 수입니다. 이 값을 조절하여 게임에서 사용할 장대의 수를 바꿀 수 있습니다.

**NUM_OF_DISKSNUM_OF_POLES의 값을 변경하게 되면 게임의 가로x세로 크기가 달라지게 됩니다. 이 경우 screenSettingcolumnrow 값을 조절하여 화면의 크기를 게임의 크기에 맞게 바꿀 수 있습니다.

  • COLUMN_WIDTH: 원판의 수에 따른 원판의 최대 크기입니다. 2n+1(n은 원판의 수)로 계산되며 장대들의 간격을 조절하기 위해 이 값이 사용됩니다.
  • COLUMN_GAP: 장대들의 간격에 추가적으로 간격을 넓혀 주는 값입니다.
  • FRAME_X, FRAME_Y: 화면에서 게임의 위치를 조절할 수 있는 값입니다.
  • DISK_COLORS: 원판들의 색깔값입니다.
  • KEYSET: 게임에 사용될 키 값들입니다. 키값을 찾기위해서는 TMI를 debug용으로 생성하고(TMI = new TM.InputManager(첫번째_인자, 두번째_인자)에서 두번째_인자true가 들어가게 설정) 게임 화면을 마우스로 클릭한 후에 키값을 알고자 하는 키보드 키를 누릅니다. 이렇게 하면 브라우저 콘솔에 해당 키의 값이 표시됩니다.
  • DISK_CHAR, CURSOR_CHAR, GROUND_CHAR, POLE_CHAR: 각각 원판, 커서, 바닥, 장대를 출력하기 위한 텍스트입니다.
  • towerData: 2차원 배열(towerData[장대_위치][원판_위치])로 원판의 값들을 저장하는 변수입니다. resetTowerData함수에 의해 초기화 됩니다.
  • moveCount: 원판이 몇 번 이동하였는지를 저장하는 변수입니다.
  • cursorPosition: 커서가 몇 번째 장대 위에 있는지를 저장하는 변수입니다. 0이 첫번째 장대위치입니다.
  • cursorDiskValue: 커서가 원판을 가지고 있는지 아닌지, 몇 칸짜리 원판을 가지고 있는지를 저장하는 변수입니다. 값이 0이면 원판을 가지고 있지 않음, 값이 있으면 해당 값의 원판을 가지고 있다는 뜻입니다.
  • isGameOver: 현재 게임이 끝난 상태인지 아닌지를 true/false로 저장합니다.

다음으로 함수들을 살펴봅시다.

function resetTowerData() {
  var towerData = [];
  for(var i=0; i<NUM_OF_POLES; i++){
    towerData[i] = [];
  }
  for(var j=0; j<NUM_OF_DISKS; j++){
    towerData[0].push(NUM_OF_DISKS-j);
  }
  return towerData;
}

towerData를 초기화하는 함수입니다. for(var i=0; i<NUM_OF_POLES; i++) 반복문으로 각각 장대를 초기화 하고, for(var j=0; j<NUM_OF_DISKS; j++) 반복문으로 첫번째 장대(towerData[0])에 원판의 값을 넣습니다. 반복이 될때마다 원판 개수(NUM_OF_DISKS)에 반복된 횟수(j)를 뺀 만큼을 원판의 값으로 넣어줍니다.

이렇게 초기화 된 배열은 다음과 같은 값을 가지는 이차원 배열이 됩니다.

[[4,3,2,1], [], []] 

towerData[장대_위치][원판_위치]=원판의_값가으로 사용되는데, 장대_위치는 왼쪽부터, 원판_위치는 아래쪽부터 시작되어 towerData[0][0]이 가장 왼쪽 장대의 가장 아래쪽 원판의 값이 됩니다. 즉 가장 왼쪽 장대에 원판이 아랫쪽부터 4, 3, 2, 1의 값을 가지는 원판들이 쌓입니다. 이 값들이 실제 화면에 출력될 때는 2n+1(n은 원판의 값)으로 변화되어 9, 7, 5, 3의 길이를 가지는 원판 텍스트가 아래에서 위로 순서대로 출력됩니다.

function getDiskColor(diskValue){
  var diskColor;
  if(diskValue>0){
    diskColor = DISK_COLORS[(diskValue-1)%(DISK_COLORS.length)];
  }
  return diskColor;
}

원판의 색깔을 가져오는 함수입니다. 원판의 값을 val로 받아서 DISK_COLORS 배열을 돌면서 색깔을 가져옵니다.

이제부터 나올 함수 이름 앞에 draw가 붙은 함수들은 화면에 텍스트를 출력하는 함수들입니다.

function drawFrame(){
  //draw static text
  TMS.insertTextAt(FRAME_X,FRAME_Y+NUM_OF_DISKS+5,"MOVES : \n\n\n");
  TMS.cursor.move(FRAME_X+11,FRAME_Y+NUM_OF_DISKS+7);
  TMS.insertText("┌──────────────────┐\n");
  TMS.insertText("│ HANOI TOWER GAME │\n");
  TMS.insertText("└──────────────────┘\n");
  TMS.insertText("www.A-MEAN-Blog.com");
  TMS.insertTextAt(FRAME_X+5,FRAME_Y+NUM_OF_DISKS+12,"KEYS : ENTER, ←, →, ESC(restart)");

  var x,y;

  //draw top of poles
  for(var i=0; i<NUM_OF_POLES; i++){
    x = FRAME_X+COLUMN_GAP+NUM_OF_DISKS+i*(COLUMN_WIDTH+COLUMN_GAP);
    y = FRAME_Y+3;
    TMS.insertTextAt(x,y,POLE_CHAR);
  }

  //draw ground
  var width = COLUMN_WIDTH*NUM_OF_POLES+COLUMN_GAP*(NUM_OF_POLES+1);
  for(var j=0; j<width; j++){
    x = FRAME_X+j;
    y = FRAME_Y+NUM_OF_DISKS+4;
    TMS.insertTextAt(x,y,GROUND_CHAR);
  }
}

게임을 진행하는 동안 바뀌지 않는 텍스트를 출력하는 함수입니다. TMS.insertTextAt 함수는 x,y 위치에 텍스트를 출력하고, TMS.cursor.move 함수는 커서 위치를 옮기며, TMS.insertText 함수는 현재 커서 위치에 텍스트를 출력합니다. TMS의 모든 함수들과 자세한 설명은 TM.ScreenManager 문서에서 확인할 수 있습니다.

//draw static text은 단순히 텍스트를 출력하는 부분이므로 설명을 생략하고, //draw top of poles과 //draw ground를 아래 그림을 참고하여 살펴봅시다.

자바스크립트 하노이 탑 쌓기/하노이 타워 게임 장대 및 바닥 위치 설명 스크린샷

장대들의 윗쪽 끝부분 텍스트는 게임의 진행중에 절대 변하지 않습니다. 하지만 그 아래 부분들은 원판이나 장대로 바뀔 수 있는 부분들이죠. 장대의 수나 장대간의 거리는 NUM_OF_DISKSNUM_OF_POLES 값을 어떻게 설정하는지에 따라 바뀌게 됩니다. 마찬가지로 바닥(ground)의 길이 역시 NUM_OF_DISKSNUM_OF_POLES 값에 따라 바뀌게 됩니다.

  //draw top of poles
  for(var i=0; i<NUM_OF_POLES; i++){
    x = FRAME_X+COLUMN_GAP+NUM_OF_DISKS+i*(COLUMN_WIDTH+COLUMN_GAP);
    y = FRAME_Y+3;
    TMS.insertTextAt(x,y,POLE_CHAR);
  }

장대 끝부분의 수는 NUM_OF_POLES와 같으므로 for 반복문으로 NUM_OF_POLES수만큼 찍어주게 됩니다. x 좌표에서FRAME_X+COLUMN_GAP+NUM_OF_DISKS가 화면 왼쪽 끝부터 첫번째 장대까지의 거리, i*(COLUMN_WIDTH+COLUMN_GAP)가 장대 사이의 간격입니다.

  //draw ground
  var width = COLUMN_WIDTH*NUM_OF_POLES+COLUMN_GAP*(NUM_OF_POLES+1);
  for(var j=0; j<width; j++){
    x = FRAME_X+j;
    y = FRAME_Y+NUM_OF_DISKS+4;
    TMS.insertTextAt(x,y,GROUND_CHAR);
  }

바닥의 길이는 COLUMN_WIDTH를 장대의 수만큼 곱한 것과 COLUMN_GAP을 장대의 수+1 만큼 곱한 것의 합입니다. COLUMN_GAP은 바닥이 시작할 때, COLUMN_WIDTH사이에, 바닥의 끝 부분에 들어가게 되서 장대의 수+1가 됩니다.

계속해서 다음 함수를 살펴봅시다.

function drawMove(){
  var x = FRAME_X+8;
  var y = FRAME_Y+NUM_OF_DISKS+5;
  TMS.insertTextAt(x,y,moveCount);
}

moveCount의 값을 출력하는 함수입니다. "MOVES :" 다음에 오도록 x, y값을 맞췄습니다.

function drawCursor(){
  for(var i=0; i<NUM_OF_POLES; i++){
    for(var j=0; j<COLUMN_WIDTH; j++){
      var x = FRAME_X+COLUMN_GAP+i*(COLUMN_WIDTH+COLUMN_GAP)+j;
      var y = FRAME_Y+1;
      var char = " ";
      var charColor = null;
      if(i===cursorPosition){
        if(cursorDiskValue === 0 && j==NUM_OF_DISKS){
          char = CURSOR_CHAR;
        }
        else if(cursorDiskValue !== 0 && j>=NUM_OF_DISKS-cursorDiskValue && j<=NUM_OF_DISKS+cursorDiskValue){
          char = DISK_CHAR;
          charColor = getDiskColor(cursorDiskValue);
        }
      }
      TMS.insertTextAt(x,y,char,charColor);
    }
  }
}

커서를 그리는 함수입니다. for(var i=0; i<NUM_OF_POLES; i++)을 통해 각각 장대를 반복하고, for(var j=0; j<COLUMN_WIDTH; j++)COLUMN_WIDTH만큼의 공간에 커서를 그리거나, 현재 커서에 원판이 선택된 경우(cursorDiskValue>0) 원판을 그리거나, 이전 커서나 원판를 지우기 위해 빈칸을 출력합니다.

x, y는 텍스트를 출력할 위치이고, char는 " "(빈칸), charColor는 null(null인 경우 기본색 출력)으로 기본값이 지정되어 있습니다. 반복문 실행 중에 장대의 순서(i)가 현재 커서가 위치한 장대(cursorPosition)인 경우 (if(i===cursorPosition)에 해당하는 경우) 다시 아래의 조건에 해당한다면 char, charColor의 값이 변경됩니다.

if(cursorDiskValue === 0 && j==NUM_OF_DISKS){
  char = CURSOR_CHAR;
}

커서를 그리는 조건입니다. cursorDiskValue === 0는 커서가 원판을 가지고 있지 않음을 뜻하고, j==NUM_OF_DISKS는 현재 텍스트 위치(j)가 COLUMN_WIDTH의 한가운데임을 뜻합니다. (COLUMN_WIDTH2*NUM_OF_DISKS+1이므로 NUM_OF_DISKS의 값이 COLUMN_WIDTH의 한가운데 값이 됩니다)

else if(cursorDiskValue !== 0 && j>=NUM_OF_DISKS-cursorDiskValue && j<=NUM_OF_DISKS+cursorDiskValue){
  char = DISK_CHAR;
  charColor = getDiskColor(cursorDiskValue);
}

원판을 그리는 조건입니다. cursorDiskValue !== 0는 커서가 원판을 가지고 있음을 뜻하고, j>=NUM_OF_DISKS-cursorDiskValue && j<=NUM_OF_DISKS+cursorDiskValue는 원판을 그려야 하는 위치안에 있음을 나타냅니다.

현재 예제에서 처럼 NUM_OF_DISKS가 4인 경우 COLUMN_WIDTH는 2n+1으로 9가 됩니다. 예를 들어 cursorDiskValue가 2라고 하면 xx#####xx (x는 빈칸을 나타냄)를 그려야 합니다. j가 0, 1일 때는 빈칸을, j가 2에서6일 때는 원판(#)을, j가 7, 8일 때는 다시 빈칸을 그려야 하는 것이죠. 위에서 설명한 것 처럼NUM_OF_DISKS이 한가운데 값이 되므로 NUM_OF_DISKS-cursorDiskValue이 원판의 시작부분, NUM_OF_DISKS+cursorDiskValue이 원판의 끝부분이 됩니다.

위 조건에 들지 않는 모든 경우에는 빈칸 " "을 출력합니다. 빈칸은 만약 기존위치에 이전에 출력된 텍스트가 있었다면 그것들을 지우는 역할도 합니다.

function drawTower(){
  for(var i=0; i<NUM_OF_POLES; i++){
    for(var j=0; j<NUM_OF_DISKS; j++){
      for(var k=0; k<COLUMN_WIDTH; k++){
        var x = FRAME_X+COLUMN_GAP+i*(COLUMN_WIDTH+COLUMN_GAP)+k;
        var y = FRAME_Y+4+NUM_OF_DISKS-1-j;
        var char = " ";
        var charColor = null;
        if(!towerData[i][j] && k==NUM_OF_DISKS){
          char = POLE_CHAR;
        }
        else if(towerData[i][j] !== 0 && k>=NUM_OF_DISKS-towerData[i][j] && k<=NUM_OF_DISKS+towerData[i][j]){
          char = DISK_CHAR;
          charColor = getDiskColor(towerData[i][j]);
        }
        TMS.insertTextAt(x,y,char,charColor);
      }
    }
  }
}

towerData를 화면에 원판과 장대로 그리는 함수입니다. 구조는 drawCursor와 유사한데 모든 장대 위치, 모든 원판 위치, COLUMN_WIDTH를 돌기 때문에 3중 반복문이 되었습니다. (drawCursor는 모든 장대 위치의 COLUMN_WIDTH를 돌기 때문에 2중 반복문) x좌표의 값은 drawCursor와 같은 식이 사용되었고, y좌표를 좀 더 자세히 살펴봅시다.

drawFrame에서 FRAME_Y+3위치에 top of poles를 출렸했으므로, 바로 밑(FRAME_Y+4)이 출력위치가 됩니다. 우리는 원판을 밑에서 부터 쌓아야 되니까 원판 높이(NUM_OF_DISKS-1)를 더하고 거기에서 현재 반복된 값(j)을 빼면 FRAME_Y+4+NUM_OF_DISKS-1-j가 y 좌표의 값이 됩니다.

function drawGameOver(){
  TMS.cursor.move(FRAME_X,FRAME_Y);
  TMS.insertText(" Completed! Your moves : "+moveCount+" \n",null,"gray");
  TMS.insertText(" Press <ESC> key to start new game ",null,"gray");
}

게임이 종료된 경우 텍스트를 추가하는 함수입니다.

function reset(){
  towerData = resetTowerData();
  moveCount = 0;
  cursorPosition = 0;
  cursorDiskValue = 0;
  isGameOver = false;

  TMS.cursor.hide();
  TMS.clearScreen();
  drawFrame();
  drawMove();
  drawCursor();
  drawTower();
}

게임의 화면과 변수들을 초기화 시키는 함수입니다. TMS.cursor.hide는 TM의 깜박이는 커서를 숨기고, TMS.clearScreen 함수는 화면의 모든 값을 지웁니다.

reset();

지금까지는 함수와 변수를 지정하였고, reset함수를 실행하는 부분입니다.

TMD.print('debug-data',{
  moveCount: moveCount,
  cursorPosition: cursorPosition,
  cursorDiskValue: cursorDiskValue,
  isGameOver: isGameOver,
});

마지막으로 TMD(TM.DebugManager의 인스턴스)으로 디버그용 데이터를 출력합니다. 아래 스크린샷에서 화면 오른쪽의 텍스트 부분(--debug-data--)입니다. 이 TMP.print 함수는 함수가 호출되는 순간의 해당 값들을 출력하게 됩니다. 즉 지금 상태로는 실시간으로 값을 업데이트하지 않습니다. 실시간으로 이 값들을 업데이트하는 코드는 하편에 나옵니다.

자바스크립트 하노이 탑 쌓기/하노이 타워 게임 브라우저 스크린샷

예제 코드의 실행

이 화면은 실제 실행중인 프로그램 화면으로 현재 페이지의 브라우저 콘솔을 연 다음 아래의 명령어들을 입력하여 프로그램을 조작할 수 있습니다.

//커서를 오른쪽으로 한칸 이동 후 업데이트하기
cursorPosition++; drawCursor();
//게임 종료 텍스트 출력해 보기
drawGameOver();


이어지는 하노이 탑 쌓기 게임 만들기 (하)편에서는 실제 데이터를 조작하는 함수를 만들고 키입력을 받아 게임을 완성해 보겠습니다.

댓글

댓글쓰기

이 글에 댓글을 다시려면 SNS 계정으로 로그인하세요. 자세히 알아보기

UP