24/1/17 지뢰찾기 (mine search)

2024. 1. 18. 09:00카테고리 없음

<MineSearchPage.jsx> 심화주차 팀원분의 게임코드를 보며 한수 배워보자

import React from "react";
import MineSearch from "../components/mineSearch/MineSearch";

const MineSearchPage = () => {   //실제 주소값 갖는 페이지이다.
  return (
    <div>
      <MineSearch />       //실제 화면에 보여줄 컴포넌트
    </div>
  );
};

export default MineSearchPage;

 

<MineSearch.jsx>보자보자

import { createContext, useReducer, useMemo, useEffect } from "react";
import Form from "./Form";         // 컴포넌트로 추정
import Table from "./Table";          //이하동문
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";     //우와 폰트 어썸이다. 이거 좋다던데
import { faStopwatch, faSmileWink } from "@fortawesome/free-solid-svg-icons";  //뭔지 모르지만 폰트어썸의 
import styled from "styled-components";                                                            //라이브러리적 뭐 있겄지 일단 기억

const MineSearch = () => {
  const [state, dispatch] = useReducer(reducer, initialState);   //오오오 유즈 리듀서다 이걸쓰시다니오오오
  const { tableData, halted, timer, result } = state;  //스테이트가 객체형이나 보네요 안의 값들 ㅇㅋ

  // useEffect를 통해 타이머를 1초마다 dispatch
  useEffect(() => {    //유즈이펙트 조건부로 작동하시겠구먼
    let timer;
    if (!halted) {      //halted가 없으면
      timer = setInterval(() => {
        dispatch({ type: INCREMENT_TIMER });   //이건 유즈리듀서의 디스패치로 보이는구먼
      }, 1000);  //작동시간은 1초뒤
    }
    return () => {
      clearInterval(timer);   //언마운트시에 타이머 꺼주고
    };
  }, [halted]);  //디펜던시어레이에 halted의 변화를 구독하고 있군

  //useMemo로 캐싱한  contextAPI를 사용하여 계속되는 렌더링 방지
  const value = useMemo(                      //useMemo로 객체의 변화를 통제하시는군
    () => ({ tableData: tableData, halted: halted, dispatch }),  //객체에 디스패치도 들어있네?
    [tableData, halted]     // 디펜던시의 요 두값이 변화할때만 객체를 바꾸는군
  );

  return (
    //value = {{ tableData: state.tableData, dispatch }} --> useMemo로 캐싱
    <TableContext.Provider value={value}>
      <Form />                                                     //폼컨포넌트 여기 있고
      <StTimer>
        <FontAwesomeIcon icon={faStopwatch} /> {timer}   //아하 위에서 사용하는 폰트어썸의 사용 방법이구먼
      </StTimer>                                                              //시간도 보여주는거 같고
      <Table />              //테이블 컴포넌트는 여기있고

      {state.isWin ? (                    //state의 속성중에 isWin 도 있나보구먼
        <StWin class="result-win">
          <FontAwesomeIcon icon={faSmileWink} /> {result}  //icon속성에 폰트어썸의 어떤값을 넘겨줘서
        </StWin>                                                                        //어떠한 이미지를 보여주시는 거같은데
      ) : (
        <StFail class="result-fail">{result}</StFail>       //result 값을 보여주고
      )}
    </TableContext.Provider>  //오호 여기에 유즈 컨텍스트가 있네??? 아하 유즈리듀서랑 유즈 컨텍스트의
  );                                               //복합작용으로 전역관리를 하시는 건가? 그러면 그냥 툴킷쓰시는게 낫지 않나?
};                                          //암튼 간지다.

export default MineSearch;

// styled-components //
const StTimer = styled.div`
  margin-top: 30px;
  display: flex;
  justify-content: center;
  font-size: 40px;
  gap: 20px;

  .svg-inline--fa {
    font-size: inherit;
  }
`;

const StWin = styled.div`
  font-size: 30px;
  font-weight: bold;
  color: #000000;
  color: orange;
  .fa-smile-wink {
    font-size: 50px;
  }
`;

const StFail = styled.div`
  font-size: 30px;
  font-weight: bold;
  color: #b52020;
`;

const StP = styled.p`
  display: flex;
  justify-content: center;
  color: black;
  margin-top: 50px;
`;
////////////////////////                                                 //홀리 보통 스타일드 컴포넌트에서 끝인줄 알았지만밑에 뭔가 많다.

// 초기값 세팅 (createContext)
export const TableContext = createContext({      //유즈 컨택스트를 여기서 만드셨군 객체형이네
  tableData: [],               //테이블 데이터는 배열형태로 받으시고
  dispatch: () => {},          //디스패치라는 함수도 갖고
  halted: true,             //홀티드는 정지란 뜻이니까 뭔가 멈추는 상태값인거 같네
});

// 승리 조건 설정
const initialState = {       //초기값이구먼
  tableData: [],
  timer: 0,
  result: "",
  halted: true,
  gameData: {     //행과 cell?과 지뢰
    row: 0,
    cell: 0,
    mine: 0,
  },
  isWin: false,   //이긴상태 유무 판별용 같고
};

export const START_GAME = "START_GAME";               //이거 유즈 리듀서 쓰기위한 초기 type설정때 
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,
        tableData: plantMine(action.row, action.cell, action.mine), //액션페이로드가 아니라 액션에 
        halted: false,                                                           // 각속성이 그냥 있네? 유즈리듀서는 원래 이렇게 하나?
        gameData: {
          row: action.row,
          cell: action.cell,             //여기 키값에 넣어주고
          mine: action.mine,
        },
        timer: 0,
        result: "",
      };

    case OPEN_CELL: {                             //이거는 셀을 눌렀을때 조건 같구먼
      const tableData = [...state.tableData];                                  //테이블 데이터를 선언해서 할당해주고
      //tableData[action.row] = [...state.tableData[action.row]];
      let openCount = 0;          //오픈카운트 0이라 이건 어따쓰지?

      tableData.forEach((row, i) => {    //배열매서드 포이치 쓰고 row 가 요소네?
        tableData[i] = [...row];              //테이블데이터의 각 요소에  row를 복사 붙여넣기? 왜? 이유가 있겄지
      }); // 모든 칸을 새로운 객체로 만들어 준다.  //요기 써있네
      const checked = [];
      const checkAround = (row, cell) => {  //요함수는 row와 cell을 받아서
        // 상하 좌우 필터링
        if (
          row < 0 ||
          row >= tableData.length ||
          cell < 0 ||
          cell >= tableData[0].length
        ) {
          return;      //조건 만족못하면 멈추고   //이미 연거 누를떄 작동인가?
        }
        // 못여는 칸 필터링
        if (
          [
            CODE.FLAG,
            CODE.FLAG_MINE,
            CODE.OPENED,
            CODE.QUESTION,
            CODE.QUESTION_MINE,
          ].includes(tableData[row][cell]) //배열매서드의 includes를 찾아봐서 있으면 true
        ) {
          return;   //이떄도 그냥 끝내고
        }
        // 중복 체크
        if (checked.includes(row + "," + cell)) {   //체크드도 배열이고 소괄호값있으면
          return;           //걍 종료
        } else {
          checked.push(row + "," + cell);   //종료로직들 다 통과하면 아까 빈배열이었던checked에 
        }                                                                  //이걸 넣어주네?
        let around = []; // 주변의 상태 값을 담는다
        if (tableData[row - 1]) {                           //tableData의 특정요가 존재하면 이프문 발동
          // 양 옆이 없을 때는 undefined를 배열에 담는다
          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.FLAG_MINE, CODE.MINE, CODE.QUESTION_MINE].includes(v)
        ).length;                           //around라는 배열을 필터해서   파란글씨 애들 에 v가 포함된애들로 배열만들고
                                                  //그 길이값을 갖는 count
        if (count === 0) {
          if (row > -1) {                   //카운트가 0이고 row가 -1이상이면 near라는 배열 만들고
            const near = [];
            if (row - 1 > -1) {                        //row보다 1적은애가 -1이상이면 near에 뭘 채우네
              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]);              //이제 그 near를 돌려서 code.opened와 같으면 
              }                                                   //check어라운드라는 함수? 자동 인자는 두개 흐음
            });
          }
        }
        tableData[row][cell] = count;          //카운트를 tableData에 할당해주고
      };

      checkAround(action.row, action.cell);      //요 함수에 action의 속성 두개 넣어주고

      tableData.forEach((tr, i) => {     //이중 포이치문??? ㄷㄷ 
        tr.forEach((td, j) => {            //이거 가로세로 로직때문인거 같은데? 아닌가?
          if (td >= 0) {
            openCount += 1;           //카운트 올려주고
          }
        });
      });

      let halted = false;          //여기서 이것들 선언해 주시네?
      let result = "";
      let isWin = false;

      if (
        openCount ===
        state.gameData.row * state.gameData.cell - state.gameData.mine  //오픈카운트와 이제 남은 칸들의 갯수비교로
      ) {                                                                                          //게임이 종료된건지 확인하는거군
        halted = true;
        result = <StP> {state.timer}초만에 성공하셨습니다 !! </StP>;
        isWin = true;                           //이겼다는 상태로 바꿔주고
      }

      return {
        ...state,
        tableData,         //여기리턴문 있네 솔직히 어디서 시작했는지 기억안나 ㅋㅋㅋ
        halted,                  //암튼 객체 반환  아하 케이스가 게임시작일때 로직의 반환이 이제 끝난거구나 ㅎㄷㄷ
        result,
      };
    }

    case CLICK_MINE: {                              //지뢰밟았을경우
      const tableData = [...state.tableData];       //이건 동일하고

      tableData[action.row] = [...state.tableData[action.row]];            //흐음 이것도 스프레드로 배열다시 만들어주고
      tableData[action.row][action.cell] = CODE.CLICKED_MINE;         //요고는 row번째애의 cell번째에의
                                                                                                          //를 mine으로 할당하는 거구나
      for (let i = 0; i < state.gameData.row; i++) {
        for (let j = 0; j < state.gameData.cell; j++) {       //이중 포문???
          if (
            tableData[i][j] === CODE.MINE ||              //전체 테이블에 지뢰가 있는곳 플래그가 된곳따져서
            tableData[i][j] === CODE.FLAG_MINE
          ) {
            tableData[i][j] = CODE.CLICKED_MINE;     //지뢰를 누른걸로 할당?
          }
        }
      }

      return {
        ...state,
        tableData,            //clickmine에 대한 리턴이고
        halted: true,
      };
    }
 
    case FLAG_CELL: {                                       //플래그 셀ㄷㄷ 셀 하나의 상태가 여러개라 조건도 많구먼
      const tableData = [...state.tableData];
      tableData[action.row] = [...state.tableData[action.row]];
      tableData[action.row][action.cell] =
        tableData[action.row][action.cell] === CODE.MINE    //특정 셀이 코드 마인이니?
          ? CODE.FLAG_MINE       //맞으면 플래그 마인하고 
          : CODE.FLAG;                    //아니면 코드 플래그해라

      return {
        ...state,
        tableData,                 //플래그셀의 리턴문
      };
    }

    case QUESTION_CELL: {                              //물음표셀?이라고
      const tableData = [...state.tableData];          
      tableData[action.row] = [...state.tableData[action.row]];  //한줄을 요 줄로 할당하고
      tableData[action.row][action.cell] =
        tableData[action.row][action.cell] === CODE.FLAG_MINE      //셀하나의 할당의 조건은
          ? CODE.QUESTION_MINE            //코드 플래그마인이면 퀘스쳔마인이고 아니면 걍 퀘스쳔
          : CODE.QUESTION

      return {
        ...state,
        tableData,             //그에 따른 리턴
      };
    }

    case NORMAL_CELL: {                            //그냥 셀은 당연히 있겠고 퀘스쳔 셀은 도대체 머여
      const tableData = [...state.tableData];
      tableData[action.row] = [...state.tableData[action.row]];     //위에서 보던애
      tableData[action.row][action.cell] =
        tableData[action.row][action.cell] === CODE.QUESTION_MINE
          ? CODE.MINE
          : CODE.NORMAL;            //퀘스쳔 셀과 비슷해

      return {
        ...state,
        tableData,                      //그에 따른 리턴
      };
    }

    case INCREMENT_TIMER: {        //타이머 증가로직으로 보이고
      return {
        ...state,
        timer: state.timer + 1,           //스테이트들은 그냥 두고 timer들만 올리는 구먼
      };
    }

    default:
      return state;                          //디폴트값
  }
};

export const CODE = {            //오호 CODE라는 객체가 제일 밑에 있었구먼
  MINE: -7, // 지뢰
  NORMAL: -1, // 일반
  QUESTION: -2, // 물음표
  FLAG: -3, // 깃발
  QUESTION_MINE: -4, // 지뢰 있는 칸의 물음표
  FLAG_MINE: -5, // 지뢰 있는 칸의 깃발
  CLICKED_MINE: -6, // 지뢰 클릭
  OPENED: 0, // 정상적으로 오픈한 칸 -> 0 이상이면 전부 오픈
};

const plantMine = (row, cell, mine) => {        //지뢰 심나보네
  const data = [];
  const shuffle = [];                                            //그냥 데이터와 셔플하는애로 보이는 두 배열
  // 난수 생성 후 셔플 리스트에 넣어주기
  let i = 0;
  while (i < mine) {              //와일문으로 돌아주는군 마인의 갯수과 같아 질떄까지
    const chosen = Math.floor(Math.random() * (row * cell));   //랜덤으로 row와 cell을 섞는군
    if (shuffle.includes(chosen) === false) {   //셔플안에 쵸즌이 존재안하면
      shuffle.push(chosen);    //쵸즌 넣어주고 증가
      i++;
    }
  }

  // 데이타 맵을 전부 노멀로 초기화
  for (let i = 0; i < row; i++) {       //여기도 이중포문이네
    const rowData = [];
    data.push(rowData);
    for (let j = 0; j < cell; j++) {              //각 로우의 셀을 CODE노멀로 채우는구먼
      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;
  }

  return data;
};
 

//와 진짜 돌았다 그냥 내려가면서 해석하기에는 무리가 있네요 로직들 엄청 많네요 지뢰찾기 2탄 기대해주세요.
//로직 좀더 들여다 볼꼐요