리액트로 구현한
지뢰찾기 게임(ver.2) 입니다.
- 지뢰판 세팅 -
지뢰찾기 게임(ver.2) 입니다.
- 지뢰판 세팅 -
예제는 인프런의 제로초, "조현영"님의 강의를 들으면서 공부한 내용입니다.
순수 자바스크립트로 만든 지뢰찾기 게임은 아래 포스트로 이동해주세요.
https://goddino.tistory.com/107
react로 구현한 지뢰찾기 게임 ver.1은 아래 포스트로 이동해주세요.
https://goddino.tistory.com/156
구현내용
· 지뢰 갯수 표시하기
· 빈칸들 한번에 열기
· 승리 조건 체크와 타이머
· useMemo 사용하여 최적화 하기
지뢰 갯수 표시하기, 빈칸들 한번에 열기, 승리 조건 체크와 타이머
승리조건: 클릭한 칸이 가로 * 세로 - 지뢰개수 한 값과 같으면 승리
MineSearch.jsx
import React, { useReducer, useEffect, createContext, useMemo } from "react";
import Form from "./Form";
import MineTable from "./MineTable";
export const CODE = {
MINE: -7, //지뢰
NORMAL: -1, //정상 칸
QUESTION: -2, //물음표
FLAG: -3, //깃발
QUESTION_MINE: -4, //물음표 자리가 지뢰일 경우
FLAG_MINE: -5, //깃발 자리가 지뢰일 경우
CLICKED_MINE: -6, //지뢰를 클릭했을 경우
OPENED: 0, //open된 칸, 0 이상이면 다 opened
};
export const TableContext = createContext({
//초깃값 설정
tableData: [], //배열모양
date: {
//게임 시작시 data 기록
row: 0,
cell: 0,
mine: 0,
},
result: "",
halted: true,
openedCount: 0,
dispatch: () => {}, //함수모양
});
const initialState = {
tableData: [],
timer: 0,
result: "",
halted: true, //true일 경우, 게임 stop
openedCount: 0,
};
//지뢰판 만들기
const plantMine = (row, cell, mine) => {
const candidate = Array(cell * row)
.fill()
.map((item, i) => i); //0-99 숫자 중에서
const mixData = [];
while (candidate.length > row * cell - mine) {
//지뢰 갯수 만큼 랜덤으로 추출
const chosen = candidate.splice(
Math.floor(Math.random() * candidate.length),
1
)[0];
mixData.push(chosen);
//console.log("mixData", mixData);
}
//2차원 배열 지뢰판 세팅
const data = [];
//판 만들고 정상 칸 심기
for (let i = 0; i < row; i++) {
const rowData = [];
data.push(rowData);
for (let j = 0; j < cell; j++) {
rowData.push(CODE.NORMAL); //정상 row*cell 칸
}
}
//2차원 배열에 지뢰심기
for (let k = 0; k < mixData.length; k++) {
const ver = Math.floor(mixData[k] / cell);
const hor = mixData[k] % cell;
data[ver][hor] = CODE.MINE; //지뢰심기
}
console.log(data);
return data;
};
export const START_GAME = "START_GAME";
export const OPEN_CELL = "OPEN_CELL";
export const CLICK_MINE = "CLICK_MINE";
export const FLAG_CELL = "FLAG_CELL";
export const QUESTION_CELL = "QUESTION_CELL";
export const NORMAL_CELL = "NORMAL_CELL";
export const INCREMENT_TIMER = "INCREMENT_TIMER";
const reducer = (state, action) => {
switch (action.type) {
case START_GAME:
return {
...state,
data: {
// 게임 시작 시 data 기록
row: action.row,
cell: action.cell,
mine: action.mine,
},
openedCount: 0,
tableData: plantMine(action.row, action.cell, action.mine), //plantMine 지뢰심기 함수
halted: false,
timer: 0,
};
case OPEN_CELL: {
let openedCount = 0;
//클릭하여 칸 열기
//불변성 떄문에 추가 코드
const tableData = [...state.tableData]; //tableData 얕은 복사
// tableData[action.row] = [...state.tableData[action.row]];
//위에 한줄만 새로운 객체로 만드는 것에서 모든 줄을 새로운 객체로 적용
tableData.forEach((row, i) => {
// 클릭한 칸 뿐 아니라 모든 칸을 새로운 객체로 만들기
tableData[i] = [...state.tableData[i]];
});
const checked = [];
const checkAround = (row, cell) => {
//재귀함수
//row, cell을 매개변수화 시켜서 action.row, action.cell에서 action 삭제
// 닫힌 칸이 아니면 리턴
if (
[
CODE.OPENED,
CODE.FLAG_MINE,
CODE.FLAG,
CODE.QUESTION_MINE,
CODE.QUESTION,
].includes(tableData[row][cell])
) {
return;
}
// 상하좌우 없는 칸은 안 열기
if (
row < 0 ||
row >= tableData.length ||
cell < 0 ||
cell >= tableData[0].length
) {
return;
}
// 옆에칸 서로 검사하는 거 막아주기 - 호출 스택 터짐 방지 코드
if (checked.includes(row + "," + cell)) {
// 이미 검사한 칸이면
return;
} else {
// 아니면 checked 배열에 넣어주기
checked.push(row + "," + cell);
}
//openedCount += 1; // 칸을 열 때마다 1씩 올려주기 += 1;
//주변칸 검사 함수
//주변칸(around 총 8칸, 판의 모서리에 있을 경우는 윗칸, 아래칸 없음) 검사
let around = [];
if (tableData[row - 1]) {
around = around.concat(
tableData[row - 1][cell - 1],
tableData[row - 1][cell],
tableData[row - 1][cell + 1]
);
}
around = around.concat(
tableData[row][cell - 1],
tableData[row][cell + 1]
);
if (tableData[row + 1]) {
around = around.concat(
tableData[row + 1][cell - 1],
tableData[row + 1][cell],
tableData[row + 1][cell + 1]
);
}
//주변의 지뢰 갯수를 확인하여 표시
const count = around.filter((v) =>
[CODE.MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v)
).length;
tableData[row][cell] = count;
// 지뢰가 없으면 주변 8칸 열기
if (count === 0) {
const near = []; //주변칸 모으기
if (row - 1 > -1) {
//제일 위칸 클릭할 경우, 칸 없애주기
near.push([row - 1, cell - 1]);
near.push([row - 1, cell]);
near.push([row - 1, cell + 1]);
}
near.push([row, cell - 1]);
near.push([row, cell + 1]);
if (row + 1 < tableData.length) {
//제일 아래칸 클릭할 경우, 칸 없애주기
near.push([row + 1, cell - 1]);
near.push([row + 1, cell]);
near.push([row + 1, cell + 1]);
}
near.forEach((n) => {
//주변칸이 닫혀 있을 때만 열림
if (tableData[n[0]][n[1]] !== CODE.OPENED) {
checkAround(n[0], n[1]);
}
});
}
//if (tableData[row][cell] === CODE.NORMAL) {
//내칸이 닫힌 칸이면 카운트 주기
openedCount += 1; // 칸을 열 때마다 1씩 올려주기
//}
tableData[row][cell] = count;
};
checkAround(action.row, action.cell);
let halted = false;
let result = "";
console.log(
state.data.row * state.data.cell - state.data.mine,
state.openedCount,
openedCount
);
if (
//승리 조건
state.data.row * state.data.cell - state.data.mine ===
state.openedCount + openedCount
) {
// 승리
halted = true; // 승리 시 게임 멈추기
result = "승리하셨습니다!";
}
return {
...state,
tableData,
halted,
result,
openedCount: state.openedCount + openedCount,
};
}
case CLICK_MINE: {
const tableData = [...state.tableData]; //tableData 얕은 복사
tableData[action.row] = [...state.tableData[action.row]];
tableData[action.row][action.cell] = CODE.CLICKED_MINE; //클릭한 셀이 CODE.CLICKED_MINE으로 바뀜
return {
...state,
tableData,
halted: true,
result,
};
}
//flag cell -> question cell -> normal cell
case FLAG_CELL: {
//지뢰 있는 칸 없는 칸 분기처리
const tableData = [...state.tableData]; //tableData 얕은 복사
tableData[action.row] = [...state.tableData[action.row]];
if (tableData[action.row][action.cell] === CODE.MINE) {
//지뢰 있는 칸
tableData[action.row][action.cell] = CODE.FLAG_MINE;
} else {
//지뢰 없는 칸
tableData[action.row][action.cell] = CODE.FLAG;
}
return {
...state,
tableData,
};
}
case QUESTION_CELL: {
//지뢰 있는 칸 없는 칸 분기처리
const tableData = [...state.tableData]; //tableData 얕은 복사
tableData[action.row] = [...state.tableData[action.row]];
if (tableData[action.row][action.cell] === CODE.FLAG_MINE) {
//깃발, 지뢰 있는 칸
tableData[action.row][action.cell] = CODE.QUESTION_MINE;
} else {
//지뢰만 있는 칸
tableData[action.row][action.cell] = CODE.QUESTION;
}
return {
...state,
tableData,
};
}
case NORMAL_CELL: {
//지뢰 있는 칸 없는 칸 분기처리
const tableData = [...state.tableData]; //tableData 얕은 복사
tableData[action.row] = [...state.tableData[action.row]];
if (tableData[action.row][action.cell] === CODE.QUESTION_MINE) {
//물음표, 지뢰 있는 칸
tableData[action.row][action.cell] = CODE.MINE;
} else {
//지뢰만 있는 칸
tableData[action.row][action.cell] = CODE.NORMAL;
}
return {
...state,
tableData,
};
}
case INCREMENT_TIMER: {
return {
...state,
timer: state.timer + 1,
};
}
default:
return state;
}
};
const MineSearch = () => {
const [state, dispatch] = useReducer(reducer, initialState);
//value useMemo로 감싸 리랜더링 최적화
//dispatch는 항상 같은 값이므로 deps에 넣지 않음
const { tableData, halted, timer, result } = state;
const value = useMemo(
() => ({ tableData: tableData, halted: halted, dispatch }),
[tableData, halted]
);
useEffect(() => {
let timer;
if (halted === false) {
// 게임 시작 시 타이머 실행
timer = setInterval(() => {
dispatch({ type: INCREMENT_TIMER });
}, 1000);
}
return () => {
clearInterval(timer);
};
}, [halted]);
return (
// 랜더링 할때마다 매번 value 객체가 새롭게 리렌더링 되므로 하위 컴포넌트도 계속 리렌더링되는 문제 발생 -> useMemo로 캐싱처리
//value = 하위에 전달하는 데이터
//<TableContext.Provider value={{ tableData: state.tableData, dispatch }}>
//TableContrect.Provider value에서 data를 넘겨주었기 때문에 하위 컴포넌트에서 자유롭게 데이터를 쓸 수 있음
<TableContext.Provider value={value}>
<Form />
<div>{state.timer}</div>
<MineTable />
<div>{state.result}</div>
</TableContext.Provider>
);
};
export default MineSearch;
화면 결과
반응형
'개발 > React' 카테고리의 다른 글
[react] react로 api 이용하여 뉴스 사이트 만들기 (0) | 2021.04.19 |
---|---|
[react] react로 axios로 API 호출 (ft. promise, hooks) (2) | 2021.04.19 |
[react] react로 지뢰찾기 게임 만들기 ver.1(ft. context API, useReducer) (0) | 2021.04.14 |
[react] 리액트 테이블 게시판 만들기 ver.2 (데이터 추가, 수정, 저장, hooks, form) (0) | 2021.04.13 |
[react] 리액트 테이블 게시판 만들기 ver.1 (axios, useEffect, 글삭제) (8) | 2021.04.13 |
댓글