해갈 2023. 3. 3. 10:08

2048게임 코드리뷰(복습)

2048 게임 페이지의 최종코드를 보면서 복습을 해보도록 하겠습니다.

 

 

2048.html

 

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>2048</title>
  <style>
    #table {
      border-collapse: collapse;
      user-select: none;
    }

    #table td {
      border: 10px solid #bbada0;
      width: 116px;
      height: 128px;
      font-size: 50px;
      font-weight: bold;
      text-align: center;
    }

    #score {
      user-select: none;
    }

    .color-2 {
      background-color: #eee4da;
      color: #776e65;
    }

    .color-4 {
      background-color: #eee1c9;
      color: #776e65;
    }

    .color-8 {
      background-color: #f3b27a;
      color: white;
    }

    .color-16 {
      background-color: #f69664;
      color: white;
    }

    .color-32 {
      background-color: #f77c5f;
      color: white;
    }

    .color-64 {
      background-color: #f75f3b;
      color: white;
    }

    .color-128 {
      background-color: #edd073;
      color: #776e65;
    }

    .color-256 {
      background-color: #edcc62;
      color: #776e65;
    }

    .color-512 {
      background-color: #edc950;
      color: #776e65;
    }

    .color-1024 {
      background-color: #edc53f;
      color: #776e65;
    }

    .color-2048 {
      background-color: #edc22e;
      color: #776e65;
    }
  </style>
</head>

<body>
  <table id="table"></table>
  <div id="score">0</div>
  <button id="back">되돌리기</button>

  <script>
    const $table = document.getElementById('table');
    const $score = document.getElementById('score');
    const $back = document.getElementById('back');
    let data = [];
    const history = [];

    $back.addEventListener('click', () => {
      const prevData = history.pop();
      if (!prevData) return;
      $score.textContent = prevData.score;
  	  data = prevData.table;
      draw();
    });
    

    function startGame() {
      const $fragment = document.createDocumentFragment();
      [1, 2, 3, 4].forEach(function () {
        const rowData = [];
        data.push(rowData);
        const $tr = document.createElement('tr');
        [1, 2, 3, 4].forEach(() => {
          rowData.push(0);
          const $td = document.createElement('td');
          $tr.appendChild($td);
        });
        $fragment.appendChild($tr);
      });
      $table.appendChild($fragment);
      // console.log($table)
      put2ToRandomCell();
      draw();
    }

    function put2ToRandomCell() {
      const emptyCells = [];
      // console.log(data)
      data.forEach(function (rowData, i) {
        rowData.forEach(function (cellData, j) {
          if (!cellData) {
            emptyCells.push([i, j]);
          }
        });
      });
      // console.log(emptyCells);
      const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
      data[randomCell[0]][randomCell[1]] = 2;
    }

    function draw() {
      data.forEach((rowData, i) => {
        rowData.forEach((cellData, j) => {
          const $target = $table.children[i].children[j];
          if (cellData > 0) {
            $target.textContent = cellData;
            $target.className = 'color-' + cellData;
          } else {
            $target.textContent = '';
            $target.className = '';
          }
        });
      });
    }

    startGame();

    // data = [
    //   [2, 2, 2, 2],
    //   [4, 4, 4, 4],
    //   [1024, 1024, 8, 8],
    //   [16, 16, 16, 16],
    // ] 개발 편의를 위해

    draw();

    function moveCells(direction) {
      history.push({
        table:JSON.parse(JSON.stringify(data)),
        score: $score.textContent,
      });
      switch (direction) {
        case 'left': {
          const newData = [[], [], [], []];
          data.forEach((rowData, i) => {
            rowData.forEach((cellData, j) => {
              if (cellData) {
                const currentRow = newData[i]
                const prevData = currentRow[currentRow.length - 1];
                if (prevData === cellData) {  // 이전 값과 지금 값이 같으면
                  const score = parseInt($score.textContent);
                  $score.textContent = score + currentRow[currentRow.length - 1] * 2;
                  currentRow[currentRow.length - 1] *= -2;
                } else {
                  newData[i].push(cellData);
                }
              }
            });
          });
          console.log(newData);
          [1, 2, 3, 4].forEach((rowData, i) => {
            [1, 2, 3, 4].forEach((cellData, j) => {
              data[i][j] = Math.abs(newData[i][j]) || 0;
            });
          });
          break;
        }
        case 'right': {
          const newData = [[], [], [], []];
          data.forEach((rowData, i) => {
            rowData.forEach((cellData, j) => {
              if (rowData[3 - j]) {
                const currentRow = newData[i]
                const prevData = currentRow[currentRow.length - 1];
                if (prevData === rowData[3 - j]) {
                  const score = parseInt($score.textContent);
                  $score.textContent = score + currentRow[currentRow.length - 1] * 2;
                  currentRow[currentRow.length - 1] *= -2;
                } else {
                  newData[i].push(rowData[3 - j]);
                }
              }
            });
          });
          console.log(newData);
          [1, 2, 3, 4].forEach((rowData, i) => {
            [1, 2, 3, 4].forEach((cellData, j) => {
              data[i][3 - j] = Math.abs(newData[i][j]) || 0;
            });
          });
          break;
        }
        case 'up': {
          const newData = [[], [], [], []];
          data.forEach((rowData, i) => {
            rowData.forEach((cellData, j) => {
              if (cellData) {
                const currentRow = newData[j]
                const prevData = currentRow[currentRow.length - 1];
                if (prevData === cellData) {
                  const score = parseInt($score.textContent);
                  $score.textContent = score + currentRow[currentRow.length - 1] * 2;
                  currentRow[currentRow.length - 1] *= -2;
                } else {
                  newData[j].push(cellData);
                }
              }
            });
          });
          console.log(newData);
          [1, 2, 3, 4].forEach((cellData, i) => {
            [1, 2, 3, 4].forEach((rowData, j) => {
              data[j][i] = Math.abs(newData[i][j]) || 0;
            });
          });
          break;
        }
        case 'down': {
          const newData = [[], [], [], []];
          data.forEach((rowData, i) => {
            rowData.forEach((cellData, j) => {
              if (data[3 - i][j]) {
                const currentRow = newData[j]
                const prevData = currentRow[currentRow.length - 1];
                if (prevData === data[3 - i][j]) {
                  const score = parseInt($score.textContent);
                  $score.textContent = score + currentRow[currentRow.length - 1] * 2;
                  currentRow[currentRow.length - 1] *= -2;
                } else {
                  newData[j].push(data[3 - i][j]);
                }
              }
            });
          });
          console.log(newData);
          [1, 2, 3, 4].forEach((cellData, i) => {
            [1, 2, 3, 4].forEach((rowData, j) => {
              data[3 - j][i] = Math.abs(newData[i][j]) || 0;
            });
          });
          break;
        }
      }
      if (data.flat().includes(2048)) {  // 승리
        draw();
        setTimeout(() => {
          alert('축하합니다! 2048을 만들었습니다.');
        }, 0);
      } else if (!data.flat().includes(0)) {  // 빈칸이 없으면 패배
        alert(`${$score.textContent}점 으로 패배했습니다. 새로고침(F5) 을 통해 게임을 초기화하세요.`);
      } else {
        put2ToRandomCell();
        draw();
      }
    }
    

    window.addEventListener('keyup', (event) => {
      if (event.key === 'ArrowUp') {
        moveCells('up');
      } else if (event.key === 'ArrowDown') {
        moveCells('down');
      } else if (event.key === 'ArrowLeft') {
        moveCells('left');
      } else if (event.key === 'ArrowRight') {
        moveCells('right');
      }
    });


    let startCoord;
    window.addEventListener('mousedown', (event) => {
      startCoord = [event.clientX, event.clientY];
    });
    window.addEventListener('mouseup', (event) => {
      const endCoord = [event.clientX, event.clientY];
      const diffX = endCoord[0] - startCoord[0];
      const diffY = endCoord[1] - startCoord[1];
      if (diffX < 0 && Math.abs(diffX) > Math.abs(diffY)) {
        moveCells('left');
      } else if (diffX > 0 && Math.abs(diffX) > Math.abs(diffY)) {
        moveCells('right');
      } else if (diffX > 0 && Math.abs(diffX) <= Math.abs(diffY)) {
        moveCells('down');
      } else if (diffX < 0 && Math.abs(diffX) <= Math.abs(diffY)) {
        moveCells('up');
      }
    });

  </script>
</body>

</html>

$back.addEventListener

back 에 추가한 이벤트는 사용자가 moveCells 함수를 통해 숫자들을 움직였을 때, 실행했던 결과가 마음에 들지 않아 바로 이전의 상황 및 모습으로 돌아가고 싶을 때 이를 되돌리게 해주는 역할을 합니다.

 

console 창

더보기
console.log(prevData);
console.log(history);

 

 

   1. history 는 moveCells 함수에서 함수를 실행할 때마다 그 값이 바뀌는데, history 는 숫자를 움직였을 때(함수를 실행했을 때), 해당 숫자들의 모습을 이중 배열로 table 로 변수선언하고, 해당 점수 또한 score 로 변수선언해 table 과 score 를 history 넣은 배열입니다. 이 중 제일 최근 값인 배열의 마지막 요소값(table 과 score) 을 가져오기 위해 pop() 을 사용해  prevData 에 변하지 않는 변수로 선언합니다.

 

const prevData = history.pop();

   2. 만약 prevData 의 값이 존재하지 않아 더 이상 되돌릴 숫자들이 없다면, return 을 통해 오류를 방지합니다. 그리고, 실제로 숫자와 점수를 제 값으로 고쳐주고, draw 함수를 통해 다시 한 번 그려줍니다. 

 

if (!prevData) return;
$score.textContent = prevData.score;
data = prevData.table;
draw();

더보기
$back.addEventListener('click', () => {
  const prevData = history.pop();
  if (!prevData) return;
  $score.textContent = prevData.score;
  data = prevData.table;
  draw();
});

function startGame

startGame 함수는 게임을 처음 시작할 때 숫자판을 만들어주는 역할을 합니다. 

 

 

   1. 성능을 최적화하기 위해 createElement 가 아닌 createDocumentFragment 을 사용하므로 해당 startGame 함수 내 반복문을 실행하기 위한 작업을 해둡니다.

 

const $fragment = document.createDocumentFragment();

   2. 이중 반복문을 통해 4 * 4 숫자판을 작성합니다. createElement 를 통해 반복되고 있는  documentFragment 안에 해당 배열에 필요한 태그를 추가(append) 해 화면에 표시하게 합니다.

 

[1, 2, 3, 4].forEach(function () {
  const rowData = [];
  data.push(rowData);
  const $tr = document.createElement('tr');
  [1, 2, 3, 4].forEach(() => {
    rowData.push(0);
    const $td = document.createElement('td');
    $tr.appendChild($td);
  });
  $fragment.appendChild($tr);
});

   3. appendChild 로 메모리상에서만 존재했던 마지막으로 $table 에 한 번에 documentFragment 를 추가하는 방식을 사용합니다. 이렇게 하므로 반복문으로 일일히 화면에 표시하지 않아도 돼 성능이 최적화될 수 있습니다.

 

$table.appendChild($fragment);

   4. put2ToRandomCell 함수를 통해 숫자판에 숫자 2 를 랜덤으로 넣습니다. 그리고, draw 함수로 숫자 2를 색칠해줍니다.

 

put2ToRandomCell();
draw();
더보기
function put2ToRandomCell() {
  const emptyCells = [];
  // console.log(data)
  data.forEach(function (rowData, i) {
    rowData.forEach(function (cellData, j) {
      if (!cellData) {
        emptyCells.push([i, j]);
      }
    });
  });
  // console.log(emptyCells);
  const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
  data[randomCell[0]][randomCell[1]] = 2;
}

function put2ToRandomCell

put2ToRandomCell 함수는 2048 게임의 핵심 역할로 처음 게임을 시작할 때와 드래그 또는 방향키를 사용해 moveCells 함수를 사용할 때마다 숫자 2 를 숫자판에 랜덤하게 나타나게 하는 역할을 합니다.

 

   1. 우선, 숫자 2 를 넣기 위해서는 숫자칸에 있는 빈칸에 넣어야 하기 때문에 숫자판을 나타내는 startGame 에서 만든 data 를 이중반복문을 통해 cellData 의 값이 undefined 인지 확인하고, 그렇다면 해당 빈칸의 위치를 나타내는 i 와  j 를 배열 형태로 첫 줄에서 빈 배열로 변수 선언한 emptyCells 에 넣습니다.

 

const emptyCells = [];
data.forEach(function (rowData, i) {
  rowData.forEach(function (cellData, j) {
    if (!cellData) {
      emptyCells.push([i, j]);
    }
  });
});

   2. emptyCells 에 넣은 값 중 랜덤으로 하나 뽑기 위해 Math.random 을 사용해 랜덤을 뽑은 배열([i, j])을 randomCell 로 변수 선언합니다. 그리고, 해당 배열의 첫 번째 값, i 와 두 번째 값, j 를 data 배열의 위치에서 찾아내고, 해당 요소값을 빈 값(undefined) 에서 2 로 바꿉니다.

 

const randomCell = emptyCells[Math.floor(Math.random() * emptyCells.length)];
data[randomCell[0]][randomCell[1]] = 2;

function draw

draw 함수는 숫자 칸에 있는 숫자에 색을 칠해주는 역할을 합니다.

 

console창

더보기
console.log(data);
console.log(rowData, i);
console.log(cellData, j);

 

 

   1. 랜덤으로 숫자 2 를 넣은 숫자칸 배열인 data 를 이중 반복문을 통해 요소값을 하나씩 돌면서 해당되는 숫자 칸(td 태그)을 $target 으로 변수선언합니다. 그리고, 해당되는 숫자 칸의 요소값(cellData)이 0 이 아닌 경우, 해당 칸에 textContent 를 해당 요소값을 바꿔주고, 해당되는 태그의 class 명을 'color-요소값' 으로 바꿉니다. 그렇지 않으면, 요소값은 0 이므로 textContent 와 class 명을 비워주면 됩니다.

 

data.forEach((rowData, i) => {
  rowData.forEach((cellData, j) => {
    const $target = $table.children[i].children[j];
    if (cellData > 0) {
      $target.textContent = cellData;
      $target.className = 'color-' + cellData;
    } else {
      $target.textContent = '';
      $target.className = '';
    }
  });
});
더보기
function draw() {
  // console.log(data)
  data.forEach((rowData, i) => {
    // console.log(rowData, i)
    rowData.forEach((cellData, j) => {
      // console.log(cellData, j)
      const $target = $table.children[i].children[j];
      // console.log($target);
      if (cellData > 0) {
        $target.textContent = cellData;
        $target.className = 'color-' + cellData;
      } else {
        $target.textContent = '';
        $target.className = '';
      }
    });
  });
}

function moveCells

moveCells 함수도 2048 게임에서 핵심 역할입니다! 드래그 또는 방향키를 통해 숫자판에 있는 숫자들을 사용자가 원하는대로 움직이는 역할을 합니다. 그와 동시에 같은 숫자가 붙으면 두 수를 합쳐줘야 하는 기능도 해야합니다. 꽤나 어렵고 복잡한 코드가 예상되지만, 숫자들을 움직이는 것은 위, 아래, 왼쪽, 오른쪽 모두 같은 기능이므로 한 쪽 방향을 잘 잡고 만들어본다면 다음 방향들은 쉬울 겁니다. 

 

   1. 우선, moveCells 함수를 이용해 숫자들을 움직일 때마다 바뀐 data 배열들을 history 에 넣어야 합니다. 이유는 back 이벤트에서 이전의 모습, 즉 이전의 data 배열을 가져와 다시 숫자판을 만들 것이기 때문이죠. 하지만, 여기서 깊은 복사를 사용하는 이유는 단적으로 예를 들어, 하던대로 table: data 를 하게 된다면,

 

history  === [

  table: [data, data, data, data... ]

]

 

가 되서 data 의 값이 전혀 바뀌지 않은 채 history 에 저장될 것입니다. 이를 방지하기 위해 깊은복사를 통해 

 

history === [

  table: [data1, data2, data3... ]

]

 

를 만들어주면 됩니다. score 에는 현재점수를 계속해서 넣어주면 되겠습니다.

 

history.push({
  table:JSON.parse(JSON.stringify(data)),
  score: $score.textContent,
});

   2. 가장 기본적인 조건문은 if입니다. 하지만 조건식에서 비교할 값이 많다면 코드가 길어지고 가독성이 떨어진다는 단점이 있습니다. 이럴 땐 switch를 사용하는 것이 좋습니다. 지금의 moveCells 함수의 상황도 마찬가지입니다. 우선, left 의 경우입니다. 이중 배열인 data 배열을 이중 반복문을 통해 하나씩 돌아가며 다음 조건들을 검사합니다.

 

해당 요소값(cellData) 이 0 이 아닌 값이 존재한다면, 현재 가로 줄 배열은 앞의 줄에서 선언한 이중 배열인 newData 의 해당 i 번째 배열이라고 currentRow 에 변수 선언합니다. 그리고, 현재 가로줄 배열(currentRow) 에서 배열의 마지막 값은 이전의 값이라고 prevData 에 변수 선언합니다. 마지막값으로 표현한 이유는 현재 newData 의 배열 안에 배열에는 값이 존재하지 않기 때문에 밑에 else { ...} 를 통해 들어온 마지막 값이 해당 요소값의 바로 이전의 값이기 때문입니다. 

 

이전의 값과 지금 값이 같으면 현재 점수를 score 로 변수 선언하고, 현재 점수와 가로줄 배열의 마지막 값(= 이전 값)을 2로 곱한 값을 더합니다. 그리고, 가로줄 배열의 마지막 값에 -2 를 곱합니다.

 

이전의 값과 현재 값이 같지 않다면, newData 의 해당 요소값의 가로줄인 i 번째 배열에 요소값을 넣습니다.

 

if (cellData) {
  const currentRow = newData[i]
  const prevData = currentRow[currentRow.length - 1];
  console.log(currentRow)
  console.log(prevData)
  if (prevData === cellData) {  // 이전 값과 지금 값이 같으면
    const score = parseInt($score.textContent);
    $score.textContent = score + currentRow[currentRow.length - 1] * 2;
    currentRow[currentRow.length - 1] *= -2;
  } else {
    newData[i].push(cellData);
  }
}

   3. 그리고 data 의 배열에서 음수인 요소값이 존재하기 때문에 이를 해결하기 위해 다시 한 번 이중반복을 통해 data 의 요소값을 반복한뒤, 해당 요소값이 음수인 경우, Math.abs 를 통해 절댓값을 구해 바꾸고, 종료합니다.

 

[1, 2, 3, 4].forEach((rowData, i) => {
  [1, 2, 3, 4].forEach((cellData, j) => {
    data[i][j] = Math.abs(newData[i][j]) || 0;
  });
});
break;

 

 

→ 이 함수는 data 의 값을 직접 대입해 풀어본다면 이해가 쉽지만, 쉽게 설명해보자면 newData 라는 비어 있는 이중배열을 만든 뒤, 검사하고자 하는 data 를 이중반복을 통해 값을 하나씩 반복해 해당 요소값에 0 이 아닌 값이 존재하면, left 이기 때문에 변수 선언을 통해 해당 요소값의 가로배열을 변수 선언하고, 해당 요소값의 왼쪽에 있는 값을 prevData 로 변수 선언합니다. 그리고, 해당 요소값의 왼 쪽에 있는 값과 해당 요소값이 같다면, 해당 요소값의 2배 만큼을 점수에 더해주고, 해당 요소값을 -2 를 곱해줘 [2, 2, 4, 8] 배열이 한 번에 [16] 으로 되는 것을 방지합니다. 같지 않다면 같은 배열에 있는 다른 요소값들이 앞 순서에 있는 요소값과 비교할 수 있도록 비어 있는 이중 배열인 newData 에 해당 요소값을 해당 위치에 넣어줍니다. 그리고 다시 한번 이중 반복을 통해 data 배열의 요소값들을 하나씩 반복하고, 요소값이 음수인 경우, Math.abs 를 통해 양수로 전환해 표현하고, 종료합니다. 다음에 있을 case 'right', case 'up', case 'down' 도 같은 원리입니다. 


   4. 만약 숫자 2048 을 만들어 내 data 함수에 포함돼 게임을 승리했다면, draw 함수로 2048 이 포함된 숫자판을 만들어줍니다. 숫자판이 만들어지고 난 뒤, 알림창이 뜨게 하기 위해 이벤트 루프의 원리를 이용하여 비동기함수 setTimeout 로 alert 함수를 감싸줍니다.  

 

if (data.flat().includes(2048)) {  // 승리
  draw();
  setTimeout(() => {
    alert('축하합니다! 2048을 만들었습니다.');
  }, 0);

   5. 만약 빈칸이 더 이상 없어 패배하게 된다면, 곧바로 alert 창을 띄웁니다. 그렇지 않고, 게임이 계속해서 진행중에 있다면, 숫자 2를 빈칸 중 랜덤하게 두고, 랜덤하게 숫자 2 를 둔 칸을 포함해 다시 숫자판을 만들어줍니다.

 

} else if (!data.flat().includes(0)) {  // 빈칸이 없으면 패배
  alert(`${$score.textContent}점 으로 패배했습니다. 새로고침(F5) 을 통해 게임을 초기화하세요.`);
} else {
  put2ToRandomCell();
  draw();
}
더보기
function moveCells(direction) {
  history.push({
    table:JSON.parse(JSON.stringify(data)),
    score: $score.textContent,
  });
  switch (direction) {
    case 'left': {
      const newData = [[], [], [], []];
      data.forEach((rowData, i) => {
        rowData.forEach((cellData, j) => {
          if (cellData) {
            const currentRow = newData[i]
            const prevData = currentRow[currentRow.length - 1];
            console.log(currentRow)
            console.log(prevData)
            if (prevData === cellData) {  // 이전 값과 지금 값이 같으면
              const score = parseInt($score.textContent);
              $score.textContent = score + currentRow[currentRow.length - 1] * 2;
              currentRow[currentRow.length - 1] *= -2;
            } else {
              newData[i].push(cellData);
            }
          }
        });
      });
      [1, 2, 3, 4].forEach((rowData, i) => {
        [1, 2, 3, 4].forEach((cellData, j) => {
          data[i][j] = Math.abs(newData[i][j]) || 0;
        });
      });
      break;
    }
    case 'right': {
      const newData = [[], [], [], []];
      data.forEach((rowData, i) => {
        rowData.forEach((cellData, j) => {
          if (rowData[3 - j]) {
            const currentRow = newData[i]
            const prevData = currentRow[currentRow.length - 1];
            if (prevData === rowData[3 - j]) {
              const score = parseInt($score.textContent);
              $score.textContent = score + currentRow[currentRow.length - 1] * 2;
              currentRow[currentRow.length - 1] *= -2;
            } else {
              newData[i].push(rowData[3 - j]);
            }
          }
        });
      });
      console.log(newData);
      [1, 2, 3, 4].forEach((rowData, i) => {
        [1, 2, 3, 4].forEach((cellData, j) => {
          data[i][3 - j] = Math.abs(newData[i][j]) || 0;
        });
      });
      break;
    }
    case 'up': {
      const newData = [[], [], [], []];
      data.forEach((rowData, i) => {
        rowData.forEach((cellData, j) => {
          if (cellData) {
            const currentRow = newData[j]
            const prevData = currentRow[currentRow.length - 1];
            if (prevData === cellData) {
              const score = parseInt($score.textContent);
              $score.textContent = score + currentRow[currentRow.length - 1] * 2;
              currentRow[currentRow.length - 1] *= -2;
            } else {
              newData[j].push(cellData);
            }
          }
        });
      });
      [1, 2, 3, 4].forEach((cellData, i) => {
        [1, 2, 3, 4].forEach((rowData, j) => {
          data[j][i] = Math.abs(newData[i][j]) || 0;
        });
      });
      break;
    }
    case 'down': {
      const newData = [[], [], [], []];
      data.forEach((rowData, i) => {
        rowData.forEach((cellData, j) => {
          if (data[3 - i][j]) {
            const currentRow = newData[j]
            const prevData = currentRow[currentRow.length - 1];
            if (prevData === data[3 - i][j]) {
              const score = parseInt($score.textContent);
              $score.textContent = score + currentRow[currentRow.length - 1] * 2;
              currentRow[currentRow.length - 1] *= -2;
            } else {
              newData[j].push(data[3 - i][j]);
            }
          }
        });
      });
      console.log(newData);
      [1, 2, 3, 4].forEach((cellData, i) => {
        [1, 2, 3, 4].forEach((rowData, j) => {
          data[3 - j][i] = Math.abs(newData[i][j]) || 0;
        });
      });
      break;
    }
  }
  if (data.flat().includes(2048)) {  // 승리
    draw();
    setTimeout(() => {
      alert('축하합니다! 2048을 만들었습니다.');
    }, 0);
  } else if (!data.flat().includes(0)) {  // 빈칸이 없으면 패배
    alert(`${$score.textContent}점 으로 패배했습니다. 새로고침(F5) 을 통해 게임을 초기화하세요.`);
  } else {
    put2ToRandomCell();
    draw();
  }
}

window.addEventListener('keyup', (event) => { ... })

keyup 이벤트는 키보드 방향키를 moveCells 에서 구현한 case left, right, up, down 에 적용시키기 위한 역할입니다. 키보드이벤트죠! keyup 을 사용한 이유는 키보드를 눌렀다 뗐을 때 해당 기능을 실행시키기 위해 keyup 을 사용했습니다. 누르자마자 실행시키고 싶다면 keydown 을 사용하면 됩니다.

 

 

console 창

console.log('keyup', event);
console.log('keydown', event);
console.log('keyleft', event);
console.log('keyright', event);


   1.  위 콘솔창에서 볼 수 있듯이 키보드를 눌렀다 뗐을 때 콘솔 창에 해당 방향의 event 를 출력해보았습니다. 여기서 event 객체에서 key 를 보면 어디 방향을 눌렀는지 우리는 콘솔을 통해서도 알 수 있습니다. 이를 이용해 moveCells 함수에 사용자가 누른 키보드의 방향을 알려줍니다.

window.addEventListener('keyup', (event) => {
  if (event.key === 'ArrowUp') {
    moveCells('up');
  } else if (event.key === 'ArrowDown') {
    moveCells('down');
  } else if (event.key === 'ArrowLeft') {
    moveCells('left');
  } else if (event.key === 'ArrowRight') {
    moveCells('right');
  }
});

window.addEventListener('mousedown', (event) => { ... })

window.addEventListener('mouseup', (event) => { ... })

두 이벤트 모두 마우스 이벤트입니다. 우선, 마우스 이벤트가 2 개로 나눠진 이유는 마우스로 방향을 표현할 때에는 드래그를 이용하기 때문입니다. 쉽게 풀어 말해, mousedown 에서 클릭을 하는 순간 클릭한 지점을 좌표를 기록하고, 어느 위치에서 마우스에서 손가락을 뗀 순간, 다시 한 번 좌표를 기록해 두 좌표간의 차를 이용해 해당 드래그가 어디로 움직였는지 확인해야 하기 때문에 이벤트가 2 개로 나눠졌습니다.

 

 

   1. 위에서 설명했듯이, 마우스를 좌클릭한 지점을 startCoord 로 변수 선언합니다. 좌표를 구하는 방법은 마무리 장에서 나와있는 마우스이벤트 를 참고하세요.

 

let startCoord;
window.addEventListener('mousedown', (event) => {
  startCoord = [event.clientX, event.clientY];
});

   2. 좌클릭하고, 드래그를 통해 커서를 움직인 다음 마우스에서 손가락을 뗐을 때, mouseup 이벤트가 작동돼 해당 지점의 좌표를 endCoord 로 변수 선언합니다. 그리고 두 좌표간의 차이를

  • x 좌표는 diffX,
  • y 좌표는 diffY

로 변수 선언합니다. 그리고 조건문에서 사분면의 원리를 이용해

  • x 가 음수이고, x 의 절대값보다 y 의 절대값이 작으면 →  left,
  • x 가 양수고, x 의 절대값이  y 의 절대값보다 크면 → right,
  • x 가 양수고, x 의 절대값이 y 의 절대값보다 작거나 같으면 → down,
  • x 가 음수이고, x 의 절대값이 y 의 절대값보다 작거나 같으면 → up 

입니다.

 

window.addEventListener('mouseup', (event) => {
  const endCoord = [event.clientX, event.clientY];
  const diffX = endCoord[0] - startCoord[0];
  const diffY = endCoord[1] - startCoord[1];
  if (diffX < 0 && Math.abs(diffX) > Math.abs(diffY)) {
    moveCells('left');
  } else if (diffX > 0 && Math.abs(diffX) > Math.abs(diffY)) {
    moveCells('right');
  } else if (diffX > 0 && Math.abs(diffX) <= Math.abs(diffY)) {
    moveCells('down');
  } else if (diffX < 0 && Math.abs(diffX) <= Math.abs(diffY)) {
    moveCells('up');
  }
});
더보기
let startCoord;
window.addEventListener('mousedown', (event) => {
  startCoord = [event.clientX, event.clientY];
});
window.addEventListener('mouseup', (event) => {
  const endCoord = [event.clientX, event.clientY];
  const diffX = endCoord[0] - startCoord[0];
  const diffY = endCoord[1] - startCoord[1];
  if (diffX < 0 && Math.abs(diffX) > Math.abs(diffY)) {
    moveCells('left');
  } else if (diffX > 0 && Math.abs(diffX) > Math.abs(diffY)) {
    moveCells('right');
  } else if (diffX > 0 && Math.abs(diffX) <= Math.abs(diffY)) {
    moveCells('down');
  } else if (diffX < 0 && Math.abs(diffX) <= Math.abs(diffY)) {
    moveCells('up');
  }
});

느낀 점

 

모르는 문법이 아닌 익숙한 문법들이라 그런지 크게 어렵다고 생각한 부분은 없었습니다. 하지만, 기능을 구현하는데 있어서 큰 어려움이 있었는데요. 하고자 하는 기능을 구현하기 위해서는 컴퓨터공학적 사고가 필요하다는 것이 어떤 점인지 어렴풋이 알 것 같다는 느낌이 들었던 단계였던 같습니다. 이유는 머릿속으로는 이 게임이 어떻게 작동하고 있는지 뻔히 알지만, 이를 코드로 풀어서 써야한다는 점이 쉽지 않은 작업이었습니다. 그러면서 든 생각은 또, 그런 작업이 개발자가 되기 위해서 가장 중요한 작업이 아닐까 생각도 하고요.

 

해결할 수 있던 요인은 어려움을 겪었던 이유에서부터 출발할 수 있는데요. 코딩하면서 다른 큰 틀과 변수 선언같은 것들은 나중에 해도 할 수 있다는 생각에 처음부터 가장 어렵고 세밀한 작업부터 진행했습니다. 나중엔 기본 바탕이 없어서 이후에 코드를 쓰면서 점점 더 기존 코드가 불어나고, 산으로 가는 경험을 했습니다. 그래서 초심으로 돌아가 처음엔 게임의 순서도를 크게 짜든, 작게 짜든 능력이 되는 선에서 메모장에 본인이 보기 쉽게 만들어 두고, 이를 보면서 어렵지 않은 작업들(변수 선언, 함수 선언) 부터 한 뒤, 순서도에 맞게 하나씩 차근차근 진행하는 것이 나중에는 도움이 되었습니다.

 

숫자들이 같고, 이를 합치고, 합치고 난 뒤 기존 숫자들 중 비어 있는 자리를 비우게 해야 하고, 생각만 해도 복잡했지만, 절대 한 번에 할 수 없고, 큰 기능을 세분해 정말 작은 것부터 차근차근하면 해결해가면 이후에 코드도 좀 쉽게 풀렸습니다.