Text-RPG(CLI) - 트러블슈팅

개요

 

개인 프로젝트의 주제인 Javascript(Node.js)로 로그라이크 게임 만들기를 실시하였다.
나는 이를 응용하여 textRPG를 만드는 시간을 가졌고, 나의 textRPG에는 맵 생성을 비롯한 이동하기 로직이 포함된다. 이번 시간에는 맵 생성 시에 고난을 겪었던 이야기를 하고자 한다! 🤣🤣🤣

 

트러블 슈팅

배경

 

처음 주제를 선정했을 때 내 머릿속 흐름은 다음과 같았다...

공격, 방어... → 전투 → RPG → 미니맵...

 

아, 미니맵을 생성하자...!

 

const map = Array.from({ length: height }, () => Array(width).fill('.'));

 

위와 같이 맵을 생성하는 데에는 딱 한 줄이면 충분했다!!!

하지만 RPG 게임에 텅 비어있는 맵이 어디있겠는가!?

 

if (player.x === x && player.y === y) {
  // 플레이어의 위치
  return chalk.blue('P');
}
if (boss && boss.x === x && boss.y === y) {
  // 보스 몬스터의 위치
  return chalk.magenta('B');
}

 

플레이어 위치와 보스 위치(랜덤 생성)도 표시해주고,

 

// 장애물 추가 (연결된 경로를 방해하지 않도록)
for (let y = 0; y < height; y++) {
  for (let x = 0; x < width; x++) {
    // 처음(플레이어) 지점과 마지막(보스) 지점 제외
    if((y === 0 && x === 0) || (x === bossX && y === bossY))
      continue;
    // 30% 확률로 장애물 생성
    if (map[y][x] === '.' && Math.random() > 0.7) {
      // 장애물 추가
      map[y][x] = 'X';
    }
  }
}

 

플레이어가 진입할 수 없는 장애물도 만들었다.

 

발단

그렇게 순조롭게 맵 생성이 진행되던 중...

 

 

그림과 같은 맵이 생성되었다. 그런데 예상치 못한 버그가 발생해버렸다.

사방이 꽉 막힌 맵이 생성되어 버린 것이다!!! 보스방은 어떻게 진입하지...?

 

전개

그럼 어떻게 해야 플레이어와 보스 사이의 길을 뻥 뚫어줄 것인가?

 

구글링과 자료 조사, 웹서핑을 진행한 결과, 수많은 방법이 있다는 걸 알 수 있었다. 그 중 BFS(너비 우선 탐색) 알고리즘을 이용하여 플레이어와 보스 사이의 최단 거리를 알아내기로 결정하였다.

 

먼저 초기화부터 진행한다.

const queue = [[startX, startY]];
visited[startY][startX] = true;

 

queue 는 경로 진행 시 방문할 노드를 저장하는 큐이다. 선언과 동시에 플레이어가 시작하는 노드를 삽입해준다. 2차원 배열 visited 는 해당 노드의 방문 여부를 기록한다. 이후 queue 가 텅 빌때까지 루프를 돌린다.

 

const [x, y] = queue.shift();
...
const directions = [
  [0, 1], [1, 0], [0, -1], [-1, 0]
];
...

 

큐에서 노드를 꺼내 (x, y)로 저장하고, 상하좌우로 이동 방향을 선언한다.

 

for (const [dx, dy] of directions) {
  // 각 방향으로 이동한 노드
  const newX = x + dx;
  const newY = y + dy;
  // 유효한 범위 체크
  if (newX >= 0 && newX < width && newY >= 0 && newY < height && !visited[newY][newX]) {
    // 방문 업데이트
    visited[newY][newX] = true;
    // 방문했으니 queue에 추가
    queue.push([newX, newY]);
    ...
  }
}

 

이를 활용하여 플레이어가 보스방으로 직행하는 길이 뻥 뚫릴 줄... 알았다...

 

위기

하지만 여전히 플레이어와 보스방 사이를 가로막는 장애물이 등장하였다.
이 때 멘탈 나가는 줄... 🤬🤬🤬

 

절정

많은 고난과 시련 끝에 나는 한 가지 방법을 알아냈다. 바로 부모 노드를 통해 경로 추적을 하는 것!!!

const parent = {};

 

parent는 경로 추적을 위해 각 노드의 부모노드를 저장한다.

 

// 부모 노드 업데이트
parent[`${newX},${newY}`] = `${x},${y}`;

 

상하좌우 네 방향을 향해 새로운 노드로 이동할 때, 기존의 노드를 부모노드로 업데이트 해줬다.

 

결말

...
if (newX === endX && newY === endY) {
  // 경로 재구성
  let [px, py] = [endX, endY];
  while (parent[`${px},${py}`]) {
    [px, py] = parent[`${px},${py}`].split(',').map(Number);
    addPath(px, py);
  }

  return;
}

 

그렇게 이동하던 노드가 목표 노드(보스방 위치)에 도달하면 부모 노드를 타고 올라가 해당 경로의 모든 노드에  .  을 표시한다. 함수 addPath 는 플레이어와 보스의 최단 경로를 저장하고 장애물이 없는 일반 노드로  .  을 할당하는 함수다.

 

이후 장애물 생성 시에 플레이어와 보스방의 최단 경로에 장애물을 생성하지 못하도록 만들면 끝!

 

if (map[y][x] === '.' && Math.random() > 0.7 && !path.has(`${x},${y}`)) {
  // 장애물 추가
  map[y][x] = 'X';
}

 

path  addPath 를 통해 최단 경로를 담은 자료구조(Set - 중복방지)다.

 

마무리

이로써 모험을 위한 맵생성을 끝마칠 수 있었다. 그 후에도 업적 생성, 상점 만들기 등 다양한 시련이 찾아왔지만...

그건 다음 시간에 얘기하도록 하자...

 

'Side Projects' 카테고리의 다른 글

ORM / Low-Level Query  (0) 2024.10.29
Protocol Buffers?  (4) 2024.10.24
모듈 시스템에서 변수 바인딩 방식  (0) 2024.10.11
Chrome dino 모작 - 트러블 슈팅  (1) 2024.10.07
Prisma 다시 돌아보기  (2) 2024.09.25