리액트로 사용자 vs 컴퓨터 Tic Tac Toe 틱택토 게임 구현하기 2편
리액트로 사용자 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 문을 작성하였다.
컴퓨터가 마지막 패를 놓고 이기게끔 유도해보았다,
전처럼 내가 추가로 클릭하지 않아도 승리자 설정, 점수 반영이 잘 되는 것을 볼 수 있다!