Files
2021-03-25 21:52:18 +09:00

309 lines
8.0 KiB
JavaScript

import React, { createContext, useReducer, useMemo, useEffect } from "react";
import Form from "./Form";
import Table from "./Table";
// 칸의 상태에 따라 코드 부여
export const CODE = {
MINE: -7,
NORMAL: -1,
QUESTION: -2,
FLAG: -3,
QUESTION_MINE: -4,
FLAG_MINE: -5,
CLICKED_MINE: -6,
OPENED: 0, // 0 이상이면 다 opened
};
// context api
export const TableContext = createContext({
tableData: [],
halted: true,
dispatch: () => {},
});
// state 초기값
const initialState = {
tableData: [],
data: {
row: 0,
cell: 0,
mine: 0,
},
timer: 0,
result: "",
halted: true,
openedCount: 0,
};
// 지뢰 심는 함수
const plantMine = (row, cell, mine) => {
console.log(row, cell, mine);
const candidate = Array(row * cell)
.fill()
.map((arr, i) => {
// 0 ~ 가로*세로 만큼 숫자 배열 생성
return i;
});
const shuffle = [];
// 지뢰 갯수 만큼 지뢰 생성
while (candidate.length > row * cell - mine) {
const chosen = candidate.splice(
Math.floor(Math.random() * candidate.length),
1
)[0];
shuffle.push(chosen);
}
// 가로 * 세로 만큼의 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);
}
}
// 지뢰 심기
for (let k = 0; k < shuffle.length; k++) {
const ver = Math.floor(shuffle[k] / cell);
const hor = shuffle[k] % cell;
data[ver][hor] = CODE.MINE;
}
console.log(data);
return data;
};
// action 명
export const START_GAME = "START_GAME";
export const OPEN_CELL = "OPEN_CELL";
export const CLICKED_MINE = "CLICKED_MINE";
export const FLAG_CELL = "FLAG_CELL";
export const QUESTION_CELL = "QUESTION_CELL";
export const NORMALIZED_CELL = "NORMALIZED_CELL";
export const INCREMENT_TIMER = "INCREMENT_TIMER";
// action 동작 정의
const reducer = (state, action) => {
switch (action.type) {
// 게임 시작
case START_GAME:
return {
...state,
data: { row: action.row, cell: action.cell, mine: action.mine },
tableData: plantMine(action.row, action.cell, action.mine),
halted: false,
openedCount: 0,
result: "",
timer: 0,
};
// 지뢰 없는 칸 클릭
case OPEN_CELL: {
const tableData = [...state.tableData];
tableData.forEach((row, i) => {
tableData[i] = [...row];
});
const checked = [];
let openedCount = 0;
const checkAround = (row, cell) => {
// 닫힌 칸만 열기
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.push(row + "," + cell);
}
if (tableData[row][cell] === CODE.NORMAL) {
openedCount += 1;
}
// 주변 칸의 값을 담을 배열
let around = [
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]
);
}
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;
// 주변 칸 모두 빈칸 일 경우
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]);
}
});
}
};
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 = `${state.timer}초 만에 승리하셨습니다.`;
}
return {
...state,
tableData,
openedCount: state.openedCount + openedCount,
halted,
result,
};
}
// 지뢰 클릭
case CLICKED_MINE: {
const tableData = [...state.tableData];
tableData[action.row] = [...state.tableData[action.row]];
tableData[action.row][action.cell] = CODE.CLICKED_MINE;
return {
...state,
tableData,
halted: true,
};
}
// 기본 칸 우클릭 시 깃발 칸
case FLAG_CELL: {
const tableData = [...state.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[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 NORMALIZED_CELL: {
const tableData = [...state.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);
const { tableData, halted, timer, result } = state;
const value = useMemo(
() => ({ tableData: tableData, halted: halted, dispatch }),
[tableData, halted]
);
useEffect(() => {
let timer;
if (!halted) {
timer = setInterval(() => {
dispatch({ type: INCREMENT_TIMER });
}, 1000);
}
return () => {
clearInterval(timer);
};
}, [halted]);
return (
<TableContext.Provider value={value}>
<Form />
<div>{timer}</div>
<Table />
<div>{result}</div>
</TableContext.Provider>
);
};
export default MineSearch;