React

리액트로 사용자 vs 컴퓨터 Tic Tac Toe 틱택토 게임 구현하기 2편

히새 2024. 5. 9. 15:07

https://h2s0.tistory.com/97

 

리액트로 사용자 vs 컴퓨터 Tic Tac Toe 틱택토 게임 구현하기 1편

안녕하세요 희새입니다!일이 있어서 (?) 리액트로 틱택토 게임을 구현해야 하는데, 헷갈려서 정리하고자 + 지피티에게 물어본 것들을 더 잘 이해하기 위해서 글을 씁니다 :) 제가 구현하고자 하

h2s0.tistory.com

리액트 틱택토 게임 만들기 1편을 보고 오시면 좋습니다 ~~~

 


 

안녕하세요 히새입니당

바로 2편 시작해보겠습니다!!!

 

저번에 심판 함수가 없어서 한 줄을 만들었는데도 불구하고 계속 패를 놓았었는데요

심판 함수를 만들어서 한 줄이 완성되면 게임을 멈추고, 점수를 줘보도록 하겠습니다.

 


승, 패 구별 함수인 referee 함수이다.

referee 는 심판이라는 뜻입니다.!

 

const referee = (squares) => {
  // 3칸이 한 줄을 이루는 경우
  const 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]
  ];

  // 배열 lines 의 각 요소를 line 으로 참조하여 순회
  for ( let line of lines ) {
    // 배열 구조 분해
    const [a,b,c] = line;
    // 첫 번째 칸이 비어있지 않은지
    if ( squares[a]
        // 세 칸이 모두 O 이나 X 로 같은 값을 가지고 있는지 확인
        && squares[a] === squares[b] && squares[a] === squares[c]
    ) {
      // 위 조건이 모두 참이면 승리한 플레이어의 심볼 ( O,X ) 반환
      // 세 개 다 같은 값이기 때문에 아무거나 반환해도 승리자를 감별할 수 있음
      return squares[a];
    }
  }
  return null;
}

 

- for .. of 문을 사용한 이유 : 가독성을 위험

for문을 사용하면 index 를 사용하기 때문에 가독성이 떨어지고, forEach 문을 사용하면 line으로 이름을 할당할 수는 있지만 return 으로 반복문을 종료시키지 못한다.

 

이렇게 심판 함수를 추가해서 한 줄이 완성되면 더 이상 작동하지 않는다. ( 내꺼만 )

한 줄 됐는데 컴퓨터는 계속 패를 놓네.. 

여기에 내 차례 click 함수에는 referee 가 값을 배출? 하면 작동이 안되게 했는데,

컴퓨터 차례인 computerMove 함수에는 이 조건을 주지 않았기 때문이다.

 

// 컴퓨터 차례
const computerMove = () => {
  if (referee(squares) || currentPlayer !== 'X') {
    return;
  }
  const emptySquare = squares.map((square,i) => square === null ? i : null).filter(i => i !== null);
  if (emptySquare.length === 0) return;
  const randomSquare = emptySquare[Math.floor(Math.random() * emptySquare.length)];
  const newSquares = squares.slice();
  newSquares[randomSquare] = 'X';
  setSquares(newSquares);
  setCurrentPlayer('O');
}

그래서 이렇게 if 문을 추가하여 승/패가 갈린 경우 = referee 함수가 값을 return 한 경우와 컴퓨터 차례가 아닌 경우에는 컴퓨터가 패를 놓지 않도록 해주었다.

 

 

이렇게.! 내가 이기니까 컴퓨터도 패를 놓지 않는다 ㅎㅎ 반칙 멈춰 ~

 


이제 결과에 따라 score을 줘보자!

사용자가 이기면 +1, 컴퓨터가 이기면 -1 해줄거다.

 

승자의 상태를 추가하고, referee 함수에서 승부가 난 뒤, 승리자 상태값을 바꿔주겠다.!

// 승리자 관리
const [winner, setWinner] = useState('');

useState 로 상태 관리 코드를 추가해주고,

// 승,패 구별
const referee = (squares) => {
  const 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]
  ];
  for ( let line of lines ) {
    const [a,b,c] = line;
    if ( squares[a]
        && squares[a] === squares[b] && squares[a] === squares[c]
    ) {
      // 추가 된 부분
      setWinner(squares[a]);
      return squares[a];
    }
  }
  return null;
}

승부가 난 뒤, squares[a] 값을 winner 로 셋팅해주었다.

 

승자에 따라 score 값을 바꿔주는 useEffect

useEffect(() => {
  if (winner === 'O') {
    setScore(+1);
  } else if (winner === 'X') {
    setScore(-1);
  } else {
    setScore(score)
  }
}, [winner])

 

그리고 return 문에

<h2>winner : {winner}</h2>

 

를 추가하여 화면에서 확인할 수 있도록 해주었다.

 

실험 삼아 해보았는데, 컴퓨터가 이기는 경우에는 마지막에 내가 클릭을 해야 결과값이 나온다.

몰랐던 사실이라 마지막에 한참을 기다렸다

 

사용자가 이긴 경우에는 잘 나온다.

그리고 둘 다 winner 상태 표시와 score 반영이 잘 되는 것을 볼 수 있다!!!

 


UI 구현

기존에는 리액트 공식문서에서 제공하던 style.css 를 가져다가 썼는데 갑자기 거슬려서 조금 바꿔보았다.

웹폰트 추가하고 square 부분만 조금 바꿔주었다. ( 색깔은 나중에 또 바뀔수도있다 )

.square {
  background: #fff;
  width: 40px;
  height: 40px;
  border-radius: 4px;
  margin : 7px;
  padding: 0;
  margin-right: -1px;
  margin-top: -1px;
  float: left;
  font-size: 18px;
  font-weight: bold;
  line-height: 34px;
  text-align: center;
}

.board-row:after {
  clear: both;
  content: '';
  display: table;
}

혹시 필요하시다면.!

 


다음 게임 버튼

 

<button className='bg-white p-2 rounded-lg' onClick={nextGameClick}>Play Again</button>

먼저 return 문 안에 버튼을 만들어주고 간단한 디자인을 해준 뒤, nextGameClick 함수를 연결해주었다.

const nextGameClick = () => {
  if (!referee(squares)) {
    return;
  }
  // 보드판 초기화
  setSquares(Array(9).fill(null));
  // 컴퓨터 차례로
  setCurrentPlayer('X');
};

심판 함수가 결과물을 내놓지 않았을 시 실행하지 않는다.

결과물을 내놓았을 때 버튼을 클릭하면 보드판을 초기화하고 컴퓨터 차례로 돌려 다시 게임을 시작할 수 있게 한다.

 

게임이 끝난 뒤 버튼을 클릭하면 보드판을 초기화하고 새로운 게임을 실행한다.

여기서 찾을 수 있는 오류가 두 가지가 있는데

1. 승부가 나지 않았을 때 ( 무승부일 때 ) 점수가 -1 이 되는것

2. -1이 된 후 이겼을 때 0점이 아닌 1이 되는 것 ( 점수를 유지하고 유지한 점수에 반영되지 않는 것 )

이 있다. 이 오류들을 고쳐보도록 하자!!!

 


점수 유지하기

useEffect(() => {
  if (winner === 'O') {
    setScore(prevScore => prevScore + 1);
  } else if (winner === 'X') {
    setScore(prevScore => prevScore - 1);
  }
}, [winner])

const nextGameClick = () => {
  if (!referee(squares)) {
    return;
  }
  setWinner('');
  setSquares(Array(9).fill(null));
  setCurrentPlayer('X');
};

setScore 해주는 useEffect 에서 그 전 score 를 받아와 그 전 score 에 +1 과 -1 을 해주었다

그리고 다음게임 클릭 함수에 setWinner 를 추가해 다음판으로 넘어가면 승리자의 정보도 지워주었다

 

 


이제 무승부일 때 설정을 해주자.!

무승부인 것을 어떻게 알지 고민을 했는데 , lines 가 완성되지 않고 칸이 다 채워졌을 때인거같다.

 

// 승,패 구별
const referee = (squares) => {
  const 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]
  ];
  for ( let line of lines ) {
    // 배열 구조분해
    const [a,b,c] = line;
    // 값이 있는지, 세 칸이 모두 같은 값을 가지고 있는지 확인
    if ( squares[a]
        && squares[a] === squares[b] && squares[a] === squares[c]
    ) {
      setWinner(squares[a]);
      return squares[a];
    }
  }
  // 추가된 부분
  const isFull = squares.every(square => square !== null);
  if (isFull ) {
    setWinner('Draw');
  }
  return null;
}

// 게임 종료 후 점수 올리고 내리기
useEffect(() => {
  if (winner === 'O') {
    setScore(prevScore => prevScore + 1);
  } else if (winner === 'X') {
    setScore(prevScore => prevScore - 1);
    // 추가된 부분
  } else if (winner === 'Draw') {
    setScore(prevScore => prevScore)
  }
}, [winner])

 

칸이 다 채워져있는지 검사하고 다 채워져있으면 승자를 Draw 로 설정해준다.

그리고 점수 관리하는 useEffect 에서 winner 가 Draw 일 때에는 그냥 이 전 값을 넣어주기로 한다!

 

every 는 처음보는 메서드인데,

every는 배열의 모든 요소가 주어진 테스트 함수를 만족할 때 true를 반환하는 배열 메소드라고 한다.

배열의 모든 요소가 특정 조건을 충족해야할 때 사용하면 배열의 모든 요소를 쉽게 검사할 수 있다.

array.every(function(currentValue, index, arr), thisValue)


위에서는 square 을 배열의 각 요소 이름으로 받아서 각 square 이 null 이 아닌 조건을 충족하면 isFull 은 true 를 반환한다.

그렇게 해서 위에 squares 가 어떠한 요소도 반환하지 않으면서 isFull 이 true 이면 승자를 무승부 Draw 로 설정해준다.

 

 


 

그리고 녹화하지는 못했지만 추가로 마지막 패가 컴퓨터로 승리했을 때 내가 보드를 클릭해야지만 승자가 설정되던 이슈가 있었다. ( + 컴퓨터가 마지막 칸을 채우며 승리했을 때에는 먹통이 되어버렸음 )

 

  // 컴퓨터가 맨 처음 수를 놓게 함
  useEffect(() => {
  // 추가 된 부분
    if (referee(squares)) {
      setWinner(referee(squares));
    }
    if (currentPlayer === 'X') {
      const timer = setTimeout(computerMove, 1000);
      return () => clearTimeout(timer);
    }
  }, [currentPlayer, squares]);

 

컴퓨터가 컴퓨터 차례에서 패를 놓는 useEffect 에서 컴퓨터가 패를 놓기 전에 승자가 있으면 winner 를 설정해주는 if 문을 작성하였다.

컴퓨터가 마지막 패를 놓고 이기게끔 유도해보았다,

전처럼 내가 추가로 클릭하지 않아도 승리자 설정, 점수 반영이 잘 되는 것을 볼 수 있다!