TIL

2024.02.22 TIL #tic-tac-toe 게임

inz1234 2024. 2. 22. 23:25

오늘 틱택토 게임 만들기를 해봤다.

게임을 만드는 과정 중 어려웠던 부분을 기록하려한다.

 

내가 생각했던 로직

1.  9개의 숫자 중 3개로 빙고가 될 수 있을 만한 배열들을 추려놓는다.

2. 9개의 버튼 중에 철수(X)와 영희(O)가 차례대로 누른다.

3. 철수가 누르는 버튼의 숫자(X가 배정된 숫자 = xList)와 영희가 누른 버튼의 숫자(O가 배정된 숫자 = oList)를 각각 다른 배열에 따로 담아둔다.

4.  xList와 oList 중에서 3개를 골라서 나올 수 있는 모든 경우의 수를 구한다.

5. 만약 xList(또는 oList)에서 무작위로 3개를 고른 배열들 중 1번의 빙고가 될 수 있는 배열을 포함하면 X(철수) 또는 O(영희)가 이긴다.

 

나의 코드 

import { useState } from "react";

function App() {
  const [buttons, setButtons] = useState(["", "", "", "", "", "", "", "", ""]);

  const dapList = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [2, 4, 6],
    [0, 4, 8],
  ];

  const [whosTurn, setTurn] = useState("X");

  const [xList, setXList] = useState([]);
  const [oList, setOList] = useState([]);

  const randomSelect = (list) => {
    let arr = [];
    let n = list.length;
    for (let i = 0; i < n - 2; i++) {
      for (let j = i + 1; j < n - 1; j++) {
        for (let k = j + 1; k < n; k++) {
          arr.push([list[i], list[j], list[k]]);
        }
      }
    }
    return arr;
  };

  const whosTheWinner = (list, who) => {
    const combilist = randomSelect(list);
    if (list.length > 2) {
      for (let i = 0; i < dapList.length; i++) {
        if (
          combilist.some((arr) => arr.every((num) => dapList[i].includes(num)))
        ) {
          alert(`${who}가 승리하였습니다.`);
        }
      }
    }
  };

  const changeResult = (num) => {
    const newButtons = [...buttons];
    if (newButtons[num]) {
      alert("너님 이미 누름 ㅋ");
      return;
    }

    if (whosTurn === "X") {
      newButtons[num] = "X";
      setButtons(newButtons);

      const updatedXList = [...xList, num];
      setXList(updatedXList);
      setTurn("O");
      whosTheWinner(updatedXList, buttons[num]);
    } else {
      newButtons[num] = "O";
      setButtons(newButtons);
      const updatedOList = [...oList, num];
      setOList(updatedOList);
      setTurn("X");
      whosTheWinner(updatedOList, newButtons[num]);
    }
  };

  return (
    <>
      <div>
        <button onClick={() => changeResult(0)}>{buttons[0]}</button>
        <button onClick={() => changeResult(1)}>{buttons[1]}</button>
        <button onClick={() => changeResult(2)}>{buttons[2]}</button>
      </div>
      <div>
        <button onClick={() => changeResult(3)}>{buttons[3]}</button>
        <button onClick={() => changeResult(4)}>{buttons[4]}</button>
        <button onClick={() => changeResult(5)}>{buttons[5]}</button>
      </div>
      <div>
        <button onClick={() => changeResult(6)}>{buttons[6]}</button>
        <button onClick={() => changeResult(7)}>{buttons[7]}</button>
        <button onClick={() => changeResult(8)}>{buttons[8]}</button>
      </div>
    </>
  );
}

export default App;

 

특히 내 코드에서  combilist.some((arr) => arr.every((num) => dapList[i].includes(num)))

이 부분을 쓸 때 매우 오래걸려버렸다ㅠㅠ

- 의도했던 바는 "xList나 oList 중에서 무작위로 3개를 뽑은 배열들 중 빙고가 될 수 있는 배열을 포함하면~" 이었고,

처음에는  combilist.includes(dapList[i]) 이렇게 썼었다. 그랬더니 절대 false가 되는 것이다.

이 글을 읽는 사람들은 이걸 보고 웃을지도 모른다. 아니 이런 기본적인 걸 몰랐다고~?ㅎ 몰랐다. 아니 깜빡해버렸다.

객체형 데이터는 각자 메모리의 주소값을 가지기 때문에 -> 두 배열이 같아보여도 사실은 다른 주소값을 가진다.

 -> 두 배열은 같다고 할 수 없다 = includes로 찾을 수 없다....

 

따라서 두 배열이 같은지(?)를 따져보려면 객체형 데이터 안에 있는 단순 데이터들끼리를 비교해 봐야한다.(주소값을 가지지 않는..) 

 

그래서 결국에는 x List에서 무작위로 세 개를 뽑은 배열들(= combilist) 중에서, 빙고가 될 수 있는 배열(= dapList[i])이

combilist의, 배열 하나의(arr), 각 요소들을 모두(every)포함(그래야 똑같은거니까)하는 combi가 하나라도 있으면(some) 승자를 알리는 alert가 뜨는 것이다.

 

즉, A배열(combilist) 안의 요소와 B배열(dapList) 안의 요소가 같은지를 비교할 때는 이중 반복문을 써야하는데,

그 안의 요소들이 또 배열이라면, 배열과 배열의 직접 비교는 불가능 => A배열 안의 각 요소들을 다시 돌리면서 다른 배열이 그 요소들을 다 포함하는지를 봐야한다. 

 

휴우 매우... 복잡시럽다.

 


그래서 답 코드를 봤다.

 

진짜... 이럴 때마다 너무 벽이 느껴진다.

답 코드는 이거였다.

const WINNING_LINES = [
  [0, 1, 2],
  [3, 4, 5],
  [6, 7, 8],
  [0, 3, 6],
  [1, 4, 7],
  [2, 5, 8],
  [0, 4, 8],
  [2, 4, 6],
];

initialState: {
    board: Array(9).fill(null),
    currentPlayer: "X",
    winner: null,
    histories: [],
  }

 

>> 

...

state.board[index] = state.currentPlayer;

state.currentPlayer = state.currentPlayer === "X" ? "O" : "X";

...

>>

 

const winningLine = WINNING_LINES.find((line) => {
    const [a, b, c] = line;
    if (board[a] && board[a] === board[b] && board[a] === board[c]) {
      return board[a];
    } 

 

이건 마치 애기한테 빙고라는 게임을 알려주는 것과 같다고 생각했다.

 

1. 자 우리는 X랑 O중에 누가 이기는지를 알아볼거야~

2. board(9개의 칸)에는 O 또는 X가 들어갈거야
    => state.board[index] = state.currentPlayer;

3. 그리고 이렇게, 이렇게, 이렇게 가 다 O이거나 X면 우린 그걸 "one 빙고"라고 하는거야

4. 여기서 이렇게, 이렇게, 이렇게는 board의 순서고, 그 경우는 WINNING_LINES에 다 들어있어.

5. 근데 WINNING_LINES 을 보니 될 수 있는 경우들이 많지?

    매번 board의 0번째, 3번째, 6번째 그러고 또 board의 2번째, 5번째, 8번째 이렇게 모든 경우를 비교하기엔 너무 많으니

    이 경우들을 통틀어서 우리는 [a, b, c]라고 할거야.

    => const [a, b, c] = line;

6. 그럼 board의 a번째랑, b번째랑, c번째가 모두 같으면 "one 빙고!"라는 말이랑 같은 말이지?

     => if (board[a] && board[a] === board[b] && board[a] === board[c])

7. 그때 board의 a번째에 들어가 있는 값이 O면 O가 이기는 거고 X면 X가 이기는거라는 거지

     => return board[a];

 

 

이 답을 봤을 때 충격적이었다.

왜 나는 이런 생각을 하지 못했을까?

이미 답이 될 수 있는 경우들은 정해져있고 결국 누가 이기는지가 중요했던 것인데,

철수와 영희가 매번 뭘 고르는지는 몰라도 됐는데

철수와 영희가 매번 뭘 고르는지가 중요한 게 아니라, 빙고를 성공하는 경우(WINNING_LINES) 그걸 성공한 사람이

철수인지 영희인지가 중요했던 것인데...

결국 분석을 하자면는 눌린 버튼 숫자 간의 직접비교를 했던 것이고, 

에서는 버튼의 숫자가 몇이든 어차피 그 숫자에는 O 또는 X가 들어갈 것이니,

세 개에 모두 같은 게 들어갈 경우 그게 O인지, X인지로 풀이를 한 듯하다.

 


ㅎㅎㅎㅎㅎ현업에 종사하는 분이 그랬다.

개발자의 세계로 들어가는 문턱은 낮지만 천장은 끝도 없다고.......

이런 간단한 알고리즘만으로도 실감이 됩니다^^