항해 99 & 회고

[실전프로젝트 ] 오목 게임 로직

Haksae 2022. 3. 21. 14:59
실전프로젝트에서 구현한 오목 게임 로직을 소개합니다 :)

오목 게임 로직은 크게 7가지 입니다.

1) 캔버스로 구현된 오목판에서 유저가 클릭한 위치 값을 x, y 좌표로 데이터 받기 (FE)
2) x, y 좌표를 통해서 승패 판단하기 (FE)

3) 순서 파악하고, x,y 좌표 값을 배열로 변환하기 (BE)
4) 3*3, 4*4 체크하기 (BE)
5) 금수가 아니라면 DB에 있는 배열의 값을 해당 color 값으로 변경하기 (BE)
6) 클라이언트에게 DB에 최신화된 배열 값을 전달하기 (BE)
7) 클라이언트 창에 방금 둔 돌이 반영된 view 제공 (FE)

 

본 글에서는 위의 로직을 최대한 이해하게 쉽게 설명해보겠습니다.
(이해를 위해 쓰다보니 로직 순서와 약간 다를 수 있습니다)
아 그리고 1)~7)까지의 로직은 시간상으로 얼마 걸리진 않습니다. 0.2초정도 걸립니다.

 

1. 기본적인 오목 구현 방식

1) 오목판 1차원 배열 (BE)

  • 여러가지 오목 구현 방식이 있지만, 저희 프로젝트에서는 1차원 배열과 2차원 배열 개념으로 오목을 구현했습니다.
  • 우선 DB에 Boards 라는 컬렉션에 board라는 field를 생성합니다.
  • 그리고 게임이 시작될 때, board라는 필드에 아래의 value를 넣습니다.
const board = new Array(Math.pow(19, 2)).fill(-1);
  • 위의 board라는 변수에는 [-1, -1, -1, -1, -1 ...] 라는 값이 담겨있을 것입니다. 바로 이게 오목판을 1차원 배열로 정의한 것입니다.
  • 우선 위에서 배열의 길이(19*19)는 오목판의 길이입니다.
  • 모든 인덱스를 -1로 채웠는데, 이것은 오목 돌이 없다는 것을 의미합니다.
  • 오목돌이 없는 깨끗한 오목판을 하나 셋팅한 것으로 생각하시면 되겠습니다.
  • 이로써 서버에서 쓰는 파둑판이 생성되었습니다.

2) 오목판 좌표 (FE)

  • 이번에는 프론트엔드 쪽에서 오목판을 처리하는 방식입니다.
  • 프론트엔드에서는 오목판을 캔버스로 구현하고, 마우스의 위치 값에 따라 오목판을 아래와 같이 X, Y라는 좌표로 구현합니다

  • 가령 위의 그림에서 빨간색 공간에 돌을 준다면, 해당 돌의 위치는 { X : 3, Y : 1 }이 됩니다.

3) 1차원 배열과 2차원 배열

  • 백엔드와 프론트엔드가 오목판을 다르게 구현하는데 어떻게 서로 통신할 것인가?
  • 바로 1차원 배열과 2차원 배열의 개념을 통해서 구현합니다.
  • 가령 유저가 위의 그림의 빨간색 공간에 돌을 둬서 { X:3, Y:1 } 의 값을 서버로 전달한다고 가정해봅시다. 서버는 이를 어떻게 1차원 배열로 변환해야할까요?
  • 바로 이것을 2차원 배열처럼 생각하고, 1차원 배열로 바꾸는 방법을 통해서 변환합니다.

  • 위의 그림에 제가 화살표를 가로 방향으로 줄마다 그려놨습니다. 그리고 줄마다 길이가 19인 배열이 가로 방향을 있다고 생각해봅시다.
  • 그러면 오목판에는 길이가 19인 배열 19개가 존재한다고 할 수 있습니다. 그리고 이를 다르게 생각해보면 오목판 자체가 바로 2차원이라고 할 수 있습니다. 바로 이를 아이디어로 오목판을 통신합니다.
xyToIndex = (x, y) => {
  return x + y * 19;
};
  • 다시 위의 이야기로 돌아가서,  { X : 3, Y : 1} 이라는 값이 서버로 오면, 서버는 위의 코드를 이용하여, 이를 2차원 배열에서 1차원 배열로 바꾸어주는 작업을 해줍니다.
  • 즉 { X : 3, Y : 1 } 이 위의 함수를 통해, 3 + 1 * 19 = 22 라는 값이 나오게 됩니다.
  • 그러면 서버에서는 DB에 저장되어있는 오목판(위에서 말했던 1차원 배열)을 불러와서 22번째 인덱스의 값을 -1에서 1(흑돌) 혹은 2(흰돌)로 변경해줍니다.

2. 승패 구현 방식

그렇다면 이번에는 승패 구현 방식에 대해서 설명하려합니다.

 

1) 방향 설정

  • 아시다시피 오목의 승패는 위와 같이 가로, 세로, 대각선, 총 4방향 중에 한 방향에 연달아 동일한 색깔의 돌을 5개 두면 승리하게됩니다.
  • 이를 위해 아래와 같이 먼저 각 4가지 방향을 변수값으로 할당해줍니다.
  • 아래 배열 값의 0을 위의 그림에 정 중앙이라고 생각하시면? 조금 더 이해가 편할 것 같습니다.
 const checkDirection = [ [1, -1], [1, 0], [1, 1], [0, 1], [-1, 1], [-1, 0], [-1, -1], [0, -1] ];

 

2) 승패 판정

a) 방향 체크하여 승패 판정

      function checkWin(x, y) {
        let thisColor = board[xyToIndex(x, y)]; // 마지막에 둔 돌의 색깔
        for (let k = 0; k < 4; k++) {        // 4방향
          let winBlack = 1;
          let winWhite = 1;
          for (let j = 0; j < 2; j++) {      // 양쪽
            for (let i = 1; i < 5; i++) {    // 4개씩 돌 색깔 체크
              let a = x + checkDirection[k + 4 * j][0] * i;
              let b = y + checkDirection[k + 4 * j][1] * i;

              if (board[xyToIndex(a, b)] === thisColor) { // 체크한 자리의 돌이 방금 둔 돌의 색과 같으면
                if (                // edge case 일시 검사 로직
                  k === 1 &&
                  parseInt(xyToIndex(a, b) / 19) !==
                    parseInt(xyToIndex(x, y) / 19)
                ) {
                  continue;
                }
                switch (thisColor) {   // 색깔에 따라서 흑,백의 숫자를 하나씩 증가
                  case 1:
                    winBlack++;
                    break;
                  case 2:
                    winWhite++;
                    break;
                }
              } else {
                break;
              }
            }
          }
          if (winBlack === 5) { // 오목이 되면 승리
            winShow(1);
          }
          if (winWhite === 5) {
            winShow(2);
          }
        }
      }

  • 코드만 넣으면 이해가 쉽지 않을 것 같아서 예시를 가져왔습니다.
  • checkWin이라는 함수 안에는 3중 포문이 들어있습니다. 3중 포문은 8가지 방향으로 승리 조건에 부합한지 체크하기 위한 로직입니다.

1) k for 문

  • 방향을 위한 for 문입니다. 가령 k가 0이면 위의 그림의 파란색 방향으로 체크 방향을 잡습니다

2) j for 문

  • 기준(그림의 빨간 원)의 양 옆을 체크하기 위한 for 문입니다
  • 가령 j가 0이면 빨간 원 기준으로 오른쪽 위의 파란색 원의 자리를 체크하고
  • j가 1이면 빨간 원 기준으로 왼쪽 아래의 파란색 원의 자리를 체크합니다

3) i for 문

  • 빨간 원 기준으로 체크하고 있는 방향으로 계속해서 체크하기 위한 for 문입니다.
  • 가령 지금 빨간 원이 { x:3, y:3 } 인데, 연두방향으로 승리 여부를 체크한다고 합시다.
  • 그러면 처음 체크할 자리는 { x:4, y:4 } 이고, 그 다음 체크할 자리는 { x:5, y:5 } 이 되어야한. 바로 이렇게 계속 체크할 수 있도록 하는 for 문입니다.

3) Edge Case

  • 3중 포문 안에 위와 같은 하나의 조건 문은 Edge Case를 위한 것입니다.
  if (board[xyToIndex(a, b)] === thisColor) { // 체크한 자리의 돌이 방금 둔 돌의 색과 같으면
    if (                // edge case 일시 검사 로직
      k === 1 &&
      parseInt(xyToIndex(a, b) / 19) !==
        parseInt(xyToIndex(x, y) / 19)

  • 3중 포문을 통해서 승리 조건을 찾을 때 만약 가로줄의 승리 조건을 찾는다면, 노란색 탐색과 같은 edge case에 빠질 수 있습니다.
  • 운 좋게 { x = 2, y = 19 } 자리에 같은 색깔의 돌이 없으면 좋겠지만, 만약 그 자리에 공교롭게도 같은 색깔의 돌이 있다면, 지금 로직에서는 잘못된 승패 결과를 낼 수 밖에 없습니다.
  • 그래서 위의 Edge Case를 위한 조건문을 통해서 가로줄을 체크할 시, 기준이 되는 돌과(빨간색 자리) 체크하는 자리(노란색)이 같은 가로줄에 있는지 체크하기 위한 조건을 추가해주었습니다.

3. 금수 체크 방법

  • 다음은 3*3, 4*4, 즉 금수 체크 로직입니다.
  • 금수 체크 로직은 브루트 포트 방식으로 다소 무식하게 구현해서.. 구현 후에 괜찮은 알고리즘으로 바꾸어보려고했는데.. 쉽지 않아서 우선 약간의 무식한 방법을 올려두기만 하겠습니다.
  • 4*4도 동일한 방식으로 구현했는데, 그건 코드가 너무 길어서 우선 3*3만 올립니다.
const check_33 = (x, y, board) => {
  let count3 = 0;
  // 가로체크.
  if (
    (board[xyToIndex(x - 3, y)] === -1 &&
      board[xyToIndex(x - 2, y)] === 1 &&
      board[xyToIndex(x - 1, y)] === 1 &&
      board[xyToIndex(x + 1, y)] === -1) ||
    (board[xyToIndex(x - 2, y)] === -1 &&
      board[xyToIndex(x - 1, y)] === 1 &&
      board[xyToIndex(x + 1, y)] === 1 &&
      board[xyToIndex(x + 2, y)] === -1) ||
    (board[xyToIndex(x - 1, y)] === -1 &&
      board[xyToIndex(x + 1, y)] === 1 &&
      board[xyToIndex(x + 2, y)] === 1 &&
      board[xyToIndex(x + 3, y)] === -1) ||
    (board[xyToIndex(x - 4, y)] === -1 &&
      board[xyToIndex(x - 3, y)] === 1 &&
      board[xyToIndex(x - 2, y)] === 1 &&
      board[xyToIndex(x - 1, y)] === -1 &&
      board[xyToIndex(x + 1, y)] === -1) ||
    (board[xyToIndex(x + 4, y)] === -1 &&
      board[xyToIndex(x + 3, y)] === 1 &&
      board[xyToIndex(x + 2, y)] === 1 &&
      board[xyToIndex(x + 1, y)] === -1 &&
      board[xyToIndex(x - 1, y)] === -1) ||
    (board[xyToIndex(x - 2, y)] === -1 &&
      board[xyToIndex(x - 1, y)] === 1 &&
      board[xyToIndex(x + 1, y)] === -1 &&
      board[xyToIndex(x + 2, y)] === 1 &&
      board[xyToIndex(x + 3, y)] === -1) ||
    (board[xyToIndex(x + 2, y)] === -1 &&
      board[xyToIndex(x + 1, y)] === 1 &&
      board[xyToIndex(x - 1, y)] === -1 &&
      board[xyToIndex(x - 2, y)] === 1 &&
      board[xyToIndex(x - 3, y)] === -1)
  )
    count3++;
  // 세로체크.
  if (
    (board[xyToIndex(x, y - 3)] === -1 &&
      board[xyToIndex(x, y - 2)] === 1 &&
      board[xyToIndex(x, y - 1)] === 1 &&
      board[xyToIndex(x, y + 1)] === -1) ||
    (board[xyToIndex(x, y - 2)] === -1 &&
      board[xyToIndex(x, y - 1)] === 1 &&
      board[xyToIndex(x, y + 1)] === 1 &&
      board[xyToIndex(x, y + 2)] === -1) ||
    (board[xyToIndex(x, y - 1)] === -1 &&
      board[xyToIndex(x, y + 1)] === 1 &&
      board[xyToIndex(x, y + 2)] === 1 &&
      board[xyToIndex(x, y + 3)] === -1) ||
    (board[xyToIndex(x, y - 4)] === -1 &&
      board[xyToIndex(x, y - 3)] === 1 &&
      board[xyToIndex(x, y - 2)] === 1 &&
      board[xyToIndex(x, y - 1)] === -1 &&
      board[xyToIndex(x, y + 1)] === -1) ||
    (board[xyToIndex(x, y + 4)] === -1 &&
      board[xyToIndex(x, y + 3)] === 1 &&
      board[xyToIndex(x, y + 2)] === 1 &&
      board[xyToIndex(x, y + 1)] === -1 &&
      board[xyToIndex(x, y - 1)] === -1) ||
    (board[xyToIndex(x, y - 2)] === -1 &&
      board[xyToIndex(x, y - 1)] === 1 &&
      board[xyToIndex(x, y + 1)] === -1 &&
      board[xyToIndex(x, y + 2)] === 1 &&
      board[xyToIndex(x, y + 3)] === -1) ||
    (board[xyToIndex(x, y + 2)] === -1 &&
      board[xyToIndex(x, y + 1)] === 1 &&
      board[xyToIndex(x, y - 1)] === -1 &&
      board[xyToIndex(x, y - 2)] === 1 &&
      board[xyToIndex(x, y - 3)] === -1)
  )
    count3++;
  // 대각선체크.
  if (
    (board[xyToIndex(x - 3, y - 3)] === -1 &&
      board[xyToIndex(x - 2, y - 2)] === 1 &&
      board[xyToIndex(x - 1, y - 1)] === 1 &&
      board[xyToIndex(x + 1, y + 1)] === -1) ||
    (board[xyToIndex(x - 2, y - 2)] === -1 &&
      board[xyToIndex(x - 1, y - 1)] === 1 &&
      board[xyToIndex(x + 1, y + 1)] === 1 &&
      board[xyToIndex(x + 2, y + 2)] === -1) ||
    (board[xyToIndex(x - 1, y - 1)] === -1 &&
      board[xyToIndex(x + 1, y + 1)] === 1 &&
      board[xyToIndex(x + 2, y + 2)] === 1 &&
      board[xyToIndex(x + 3, y + 3)] === -1) ||
    (board[xyToIndex(x - 3, y - 3)] === -1 &&
      board[xyToIndex(x - 2, y - 2)] === 1 &&
      board[xyToIndex(x - 1, y - 1)] === -1 &&
      board[xyToIndex(x + 1, y + 1)] === 1 &&
      board[xyToIndex(x + 2, y + 2)] === -1) ||
    (board[xyToIndex(x + 3, y + 3)] === -1 &&
      board[xyToIndex(x + 2, y + 2)] === 1 &&
      board[xyToIndex(x + 1, y + 1)] === -1 &&
      board[xyToIndex(x - 1, y - 1)] === 1 &&
      board[xyToIndex(x - 2, y - 2)] === -1) ||
    (board[xyToIndex(x - 4, y - 4)] === -1 &&
      board[xyToIndex(x - 3, y - 3)] === 1 &&
      board[xyToIndex(x - 2, y - 2)] === 1 &&
      board[xyToIndex(x - 1, y - 1)] === -1 &&
      board[xyToIndex(x + 1, y + 1)] === -1) ||
    (board[xyToIndex(x + 4, y + 4)] === -1 &&
      board[xyToIndex(x + 3, y + 3)] === 1 &&
      board[xyToIndex(x + 2, y + 2)] === 1 &&
      board[xyToIndex(x + 1, y + 1)] === -1 &&
      board[xyToIndex(x - 1, y - 1)] === -1)
  )
    count3++;
  // 반대 대각선체크.
  if (
    (board[xyToIndex(x + 3, y - 3)] === -1 &&
      board[xyToIndex(x + 2, y - 2)] === 1 &&
      board[xyToIndex(x + 1, y - 1)] === 1 &&
      board[xyToIndex(x - 1, y + 1)] === -1) ||
    (board[xyToIndex(x + 2, y - 2)] === -1 &&
      board[xyToIndex(x + 1, y - 1)] === 1 &&
      board[xyToIndex(x - 1, y + 1)] === 1 &&
      board[xyToIndex(x - 2, y + 2)] === -1) ||
    (board[xyToIndex(x + 1, y - 1)] === -1 &&
      board[xyToIndex(x - 1, y + 1)] === 1 &&
      board[xyToIndex(x - 2, y + 2)] === 1 &&
      board[xyToIndex(x - 3, y + 3)] === -1) ||
    (board[xyToIndex(x + 3, y - 3)] === -1 &&
      board[xyToIndex(x + 2, y - 2)] === 1 &&
      board[xyToIndex(x + 1, y - 1)] === -1 &&
      board[xyToIndex(x - 1, y + 1)] === 1 &&
      board[xyToIndex(x - 2, y + 2)] === -1) ||
    (board[xyToIndex(x - 3, y + 3)] === -1 &&
      board[xyToIndex(x - 2, y + 2)] === 1 &&
      board[xyToIndex(x - 1, y + 1)] === -1 &&
      board[xyToIndex(x + 1, y - 1)] === 1 &&
      board[xyToIndex(x + 2, y - 2)] === -1) ||
    (board[xyToIndex(x + 4, y - 4)] === -1 &&
      board[xyToIndex(x + 3, y - 3)] === 1 &&
      board[xyToIndex(x + 2, y - 2)] === 1 &&
      board[xyToIndex(x + 1, y - 1)] === -1 &&
      board[xyToIndex(x - 1, y + 1)] === -1) ||
    (board[xyToIndex(x - 4, y + 4)] === -1 &&
      board[xyToIndex(x - 3, y + 3)] === 1 &&
      board[xyToIndex(x - 2, y + 2)] === 1 &&
      board[xyToIndex(x - 1, y + 1)] === -1 &&
      board[xyToIndex(x + 1, y - 1)] === -1)
  )
    count3++;
  if (count3 > 1) return 1;
  else return 0;
};