리액트 틱택토 게임 만들기 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 문을 작성하였다.
컴퓨터가 마지막 패를 놓고 이기게끔 유도해보았다,
전처럼 내가 추가로 클릭하지 않아도 승리자 설정, 점수 반영이 잘 되는 것을 볼 수 있다!
'React' 카테고리의 다른 글
우리 아이 첫 타입스크립트 프로젝트 | React, Typescript, Tailwind 로 Todolist 만들기 1탄 | 초기개발환경 셋팅하기 (0) | 2024.05.13 |
---|---|
리액트로 사용자 vs 컴퓨터 Tic Tac Toe 틱택토 게임 구현하기 3편, 전체코드공유 (0) | 2024.05.10 |
리액트로 사용자 vs 컴퓨터 Tic Tac Toe 틱택토 게임 구현하기 1편 (0) | 2024.05.07 |
포켓베이스 pocketbase 설치하는 방법 (0) | 2023.10.28 |
React 컴포넌트 (0) | 2023.08.20 |