개발노트

08 지뢰찾기 본문

React/webgame

08 지뢰찾기

aloha2jh 2024. 4. 20. 00:07

 

 

 

 

import React, { useReducer, useMemo, useCallback, useRef, useEffect } from 'react';
import { createContext } from 'react';

import Table from './Table';
import Form from './Form';

/*
*/
export const CODE = {
    OPEN_MINE: -11, //
    OPEN: -1,    //

    CLOSE_MINE: -44,
    CLOSE: -4,

    FLAG_MINE: -22,
    FLAG: -2,

    QUESTION_MINE: -33,
    QUESTION: -3,
}


function makeTableData(col, row) {
    const data = new Array();
    //console.log(col, row);
    //let count = 1;
    for (let i = 0; i < row; i++) {
        const row = [];
        for (let j = 0; j < col; j++) {
            row.push(CODE.CLOSE);
            //count++;
        }
        data.push(row);
    }
    return data;
}
function plantMine(data, mine) {
    const col = data[0].length; //9
    const row = data.length; // 7
    let arr = data;
    const mines = getRandomNums(mine, (row * col));
    //console.log(mines);
    mines.forEach((mine) => {
        mine = mine - 1;
        const one = Math.floor(mine / col);
        const two = Math.floor(mine % col)
        arr[one][two] = CODE.CLOSE_MINE;
    });
    //console.log(arr);
    return arr;
}
function getRandomNums(count, max) {
    const nums = new Set();
    while (nums.size < count) {
        nums.add(Math.floor((Math.random() * max) + 1));
    }
    return Array.from(nums).sort((a, b) => a - b);
}
function aroundMineNums(tableData, tr, td) {
    let around = [];
    if (tableData[tr - 1]) {
        around = around.concat(
            tableData[tr - 1][td - 1],
            tableData[tr - 1][td],
            tableData[tr - 1][td + 1]
        );
    }
    around = around.concat(
        tableData[tr][td - 1], tableData[tr][td + 1],
    );
    if (tableData[tr + 1]) {
        around = around.concat(
            tableData[tr + 1][td - 1],
            tableData[tr + 1][td],
            tableData[tr + 1][td + 1]
        );
    }
    const mineCount = around.filter(
        (v) => ([CODE.CLOSE_MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].includes(v))).length;
    return mineCount;
}
const initialState = {
    tableData: [],
    timer: 0,
    youWin: false,
    gameOver: false,
    mineTotal: 0,
    cellTotal: 0,
    cellOpend: 0, //
}
export const CLICK_MAKE_BTN = 'CLICK_MAKE_BTN';
export const CLICK_NORMAL = 'CLICK_NORMAL';
export const CLICK_MINE = 'CLICK_MINE';
export const R_CLICK_FLAG = 'R_CLICK_FLAG';
export const R_CLICK_CLOSE = 'R_CLICK_CLOSE';
export const R_CLICK_QUESTION = 'R_CLICK_QUESTION';
export const INCREMENT_TIMER = 'INCREMENT_TIMER';

const reducer = (state, action) => {
    switch (action.type) {
        case CLICK_MAKE_BTN: // START game...
            return {
                ...state,
                tableData: plantMine(makeTableData(action.col, action.row), action.mine),
                cellTotal: (action.col * action.row) - action.mine,
                cellOpend: 0,
                mineTotal: action.mine,
                youWin: false,
                gameOver: false,
                timer: 0,
            }
        case CLICK_NORMAL: {

            // if (state.cellOpend + 1 === state.cellTotal
            //     && [CODE.CLOSE_MINE, CODE.FLAG_MINE, CODE.QUESTION_MINE].indexOf(tableData[action.tr][action.td]) !== -1) {

            //}
            //1. table데이터 복사.
            const tableData = [...state.tableData];
            tableData.forEach((tr, i) => { tableData[i] = [...tr] });

            // CODE.OPEN;
            //tableData[action.tr][action.td] = aroundMineNums(tableData, action.tr, action.td);

            const checked = [];
            let openedCount = 0;
            const checkAround = (tr, td) => {
                //상하좌우 없으면 안열기-그냥 리턴
                if (tr < 0 || tr >= tableData.length || td < 0 || td >= tableData[0].length) {
                    return;
                }
                //console.log(([CODE.OPEN]).includes(tableData[tr][td]));
                //오픈되있으면 리턴.
                if ([CODE.FLAG, CODE.FLAG_MINE, CODE.QUESTION_MINE, CODE.QUESTION]
                    .includes(tableData[tr][td]) || -1 <= tableData[tr][td]) {
                    return;
                } else {
                    // 한 번 연칸은 무시하기...
                    if (checked.includes(tr + '/' + td)) {
                        return;
                    } else {
                        checked.push(tr + '/' + td);
                    }
                    const count = aroundMineNums(tableData, tr, td); // 지뢰 없으면
                    // 타겟칸의 지뢰가 0 이면 주변칸 오픈
                    if (count === 0) {
                        if (tr > -1) {
                            const near = [];
                            if (tr - 1 > -1) {
                                near.push([tr - 1, td - 1]);
                                near.push([tr - 1, td]);
                                near.push([tr - 1, td + 1]);
                            }
                            near.push([tr, td - 1]);
                            near.push([tr, td + 1]);
                            if (tr + 1 < tableData.length) {
                                near.push([tr + 1, td - 1]);
                                near.push([tr + 1, td]);
                                near.push([tr + 1, td + 1]);
                            }
                            //오픈해줄 칸 담기
                            near.forEach((n) => {
                                if (tableData[n[0]][n[1]] !== CODE.OPEN) {
                                    checkAround(n[0], n[1]);  //재귀
                                }
                            })
                        }
                    }
                    tableData[tr][td] = count;
                    openedCount += 1;
                    // console.log(`------tr-${tr} td-${td} ` + openedCount);
                }

            }
            checkAround(action.tr, action.td);

            let win = false;
            if (state.cellTotal - (state.cellOpend + openedCount) === 0) {
                win = true;
            }
            return {
                ...state,
                tableData,
                youWin: win,
                cellOpend: state.cellOpend + openedCount
            }
        }
        case CLICK_MINE: {
            const tableData = [...state.tableData];
            tableData[action.tr] = [...tableData[action.tr]];
            tableData[action.tr][action.td] = CODE.OPEN_MINE;

            //나머지 지뢰인애도 값을 OPEN_MINE으로 바꿔주기~
            return {
                ...state,
                tableData,
                gameOver: true
            }
        }
        case R_CLICK_FLAG: { // flag => normal
            const tableData = [...state.tableData];
            tableData[action.tr] = [...tableData[action.tr]];
            tableData[action.tr][action.td] = (tableData[action.tr][action.td] == CODE.FLAG_MINE)
                ? CODE.CLOSE_MINE : CODE.CLOSE;
            return {
                ...state,
                tableData
            }
        }
        case R_CLICK_CLOSE: { //close => question
            const tableData = [...state.tableData];
            tableData[action.tr] = [...tableData[action.tr]];
            tableData[action.tr][action.td] = (tableData[action.tr][action.td] == CODE.CLOSE_MINE)
                ? CODE.QUESTION_MINE : CODE.QUESTION;
            return {
                ...state,
                tableData
            }
        }
        case R_CLICK_QUESTION: { // question => flag
            const tableData = [...state.tableData];
            tableData[action.tr] = [...tableData[action.tr]];
            tableData[action.tr][action.td] = (tableData[action.tr][action.td] == CODE.QUESTION_MINE)
                ? CODE.FLAG_MINE : CODE.FLAG;
            return {
                ...state,
                tableData
            }
        }
        case INCREMENT_TIMER: {
            return {
                ...state,
                timer: state.timer += 1
            }
        }
        default:
            return state
    }
}
//contextApi로 관리할 데이터
export const TableContext = createContext({
    tableData: [],
});

const MineSearch = () => {
    const [state, dispatch] = useReducer(reducer, initialState);
    const { tableData, gameOver, cellTotal, cellOpend, youWin, timer } = state;

    const value = useMemo(() => (
        { tableData, dispatch, gameOver, youWin, timer }
    ), [tableData]);

    useEffect(() => {
        let timer;
        //처음 시작 막기 + 버튼 클릭시 시작
        if (tableData.length < 1) {
            return;
        }
        if (!gameOver && !youWin) {
            timer = setInterval(() => {
                dispatch({ type: INCREMENT_TIMER });
            }, 1000);
        } else {
            clearInterval(timer);
        }
        return () => { clearInterval(timer); }
    }, [tableData, gameOver, youWin]);

    return (
        <div className="mine-search-wrap">
            <h2>지뢰찾기</h2>
            <TableContext.Provider value={value}>
                <Form />
                <Table tableData={tableData} />
                {cellTotal !== 0 ?
                    <ul>
                        <li>남은칸 {cellTotal - cellOpend}/{cellTotal}</li>
                        {youWin && <h2>{timer}초만에 승리!</h2>}
                        <li>{timer} 초</li>
                    </ul> : null}
            </TableContext.Provider>
        </div>
    )

}
export default MineSearch;

 

 

 

import React, { useState, useCallback, useContext, memo } from 'react';
import { TableContext } from './MineSearch';

/*

-state로 row cell mine 값 저장하기
-input 마다 onChange 되면 state에 값을 set하기 
-버튼 click시 dispatch로 3개 값 상위컴포넌트로 올려주기

Q.input컴포넌트도 컴포넌트 분리가능할것같은데.
Q. useRef 필수로 써야하는거 아니였나? focus기능같은거 안넣으니 필요없는건가;
reset할때 값 비워줘야 될때 안필요하나.?
Q. 제로초가 컴포넌트에 만드는 함수마다 useCallback으로 감싸는이유모르겠음;
=> 걍 컴포넌트 안에있는 이벤트 핸들러 함수들 다 감싸는게 맞는건가;; 네 맞슴다.
불필요한 렌더링 막기 위해서 그냥 컴포넌트 안에 있는 이벤트핸들러같은경우 useCallback

*/

const Form = memo(() => {

    const value = useContext(TableContext);
    const { dispatch, tableData } = useContext(TableContext);

    const [col, setCol] = useState(5);
    const [row, setRow] = useState(5);
    const [mine, setMine] = useState(5);

    const onChangeCol = useCallback((e) => {
        setCol(e.target.value);
    }, [col]);
    const onChangeRow = useCallback((e) => {
        setRow(e.target.value);
    }, [row]);
    const onChangeMine = useCallback((e) => {
        setMine(e.target.value);
    }, [mine]);
    const onClickBtn = useCallback((e) => {
        //console.log(value);
        e.preventDefault();
        //console.log(col, row, mine);
        dispatch({ type: 'CLICK_MAKE_BTN', col: col, row: row, mine: mine });

    }, [col, row, mine]);

    return (
        <form>
            <ul>
                <li>
                    가로 <input id="col" type="number"
                        value={col} onChange={onChangeCol} />
                    세로 <input id="row" type="number"
                        value={row} onChange={onChangeRow} />
                    지뢰 <input id="mine" type="number"
                        value={mine} onChange={onChangeMine} />
                </li>
            </ul>
            <button onClick={onClickBtn}>만들기</button>
        </form>
    )
});


export default Form;

 

 

 

 

 

import React, { useCallback, useContext, memo, useMemo } from 'react';
import { TableContext } from './MineSearch';
import { CODE } from './MineSearch';
/*
( close, open, open-mine, mine, flag, question )
 -1 =>텍스트 함수하면 => '' (이거는.. 재귀써서 근처에 지뢰세서 알려주는 함수 필요 이건 나중에 우선은)
 
*/

const getTdStyle = (gameOver, tdData, youWin) => {
    console.log("getTdStyle");
    if (youWin) {
        switch (tdData) {
            case CODE.OPEN_MINE:
            case CODE.CLOSE_MINE:
            case CODE.FLAG_MINE:
            case CODE.QUESTION_MINE:
                return 'flag'
            case CODE.OPEN:
            case CODE.CLOSE:
                return 'open'
            //return 'close'
            case CODE.FLAG:
                return 'flag';
            case CODE.QUESTION:
                return 'question'
            default:
                return 'open'
        }
    }
    else if (gameOver) {
        switch (tdData) {
            case CODE.OPEN_MINE:
            case CODE.CLOSE_MINE:
            case CODE.FLAG_MINE:
            case CODE.QUESTION_MINE:
                return 'mine'
            case CODE.OPEN:
            case CODE.CLOSE:
                return 'open'
            //return 'close'
            case CODE.FLAG:
                return 'flag';
            case CODE.QUESTION:
                return 'question'
            default:
                return 'open'
        }
    } else {
        switch (tdData) {
            case CODE.OPEN_MINE:
                return 'mine'
            case CODE.OPEN:
                return 'open'
            case CODE.CLOSE:
            case CODE.CLOSE_MINE:
                return 'close'
            case CODE.FLAG:
            case CODE.FLAG_MINE:
                return 'flag';
            case CODE.QUESTION:
            case CODE.QUESTION_MINE:
                return 'question'
            default:
                return 'open'
        }
    }
}
const getTdText = (tdData) => {
    switch (tdData) {
        case CODE.FLAG:
        case CODE.FLAG_MINE:
        case CODE.OPEN:
        case CODE.OPEN_MINE:
        case CODE.CLOSE:
            return '';
        case CODE.CLOSE_MINE:
            return '___';
        case CODE.QUESTION:
        case CODE.QUESTION_MINE:
            return '?';
        default:
            return tdData == 0 ? '' : tdData;
    }
}

const Td = memo(({ tr, td, tdData }) => {

    //const value = useContext(TableContext);
    const { dispatch, tableData, gameOver, youWin } = useContext(TableContext);

    const onClickTd = useCallback(() => {
        if (gameOver) { return; }
        const cell = tableData[tr][td];
        switch (cell) {
            case CODE.OPEN:
            case CODE.MINE:
            case CODE.FLAG:
            case CODE.FLAG_MINE:
                break;
            case CODE.CLOSE_MINE:
            case CODE.QUESTION_MINE:
                dispatch({ type: 'CLICK_MINE', tr: tr, td: td });
                break;
            case CODE.CLOSE:
            case CODE.QUESTION:
                dispatch({ type: 'CLICK_NORMAL', tr: tr, td: td });
                break;
            default:
                console.warn(tr, td);
                break;
        }

    }, [tableData[tr][td], gameOver]);
    const onClickTdRight = useCallback((e) => {
        e.preventDefault();
        if (gameOver) { return; }

        const cell = tableData[tr][td];
        switch (cell) {
            case CODE.OPEN:
            case CODE.MINE:
                break;
            case CODE.FLAG_MINE:
            case CODE.FLAG:
                dispatch({ type: 'R_CLICK_FLAG', tr: tr, td: td });
                break;
            case CODE.CLOSE_MINE:
            case CODE.CLOSE:
                dispatch({ type: 'R_CLICK_CLOSE', tr: tr, td: td });
                break;
            case CODE.QUESTION_MINE:
            case CODE.QUESTION:
                dispatch({ type: 'R_CLICK_QUESTION', tr: tr, td: td });
                break;
            default:
                console.warn(tr, td);
                break;
        }

    }, [tableData[tr][td], gameOver, youWin]);

    console.log("td rendering")

    return useMemo(() => (
        <td className={`tr${tr}-td${td} ${getTdStyle(gameOver, tdData, youWin)}`}
            key={`${tr}-${td}`} onClick={onClickTd} onContextMenu={onClickTdRight}>
            {getTdText(tdData)}</td>
    ), [tableData[tr][td]]);
});

export default Td;

 

 

 

import React, { memo } from 'react';
import Tr from './Tr';

const Table = memo(({ tableData }) => {

    return (
        <table>
            <tbody>
                {tableData &&
                    tableData.map((v, i) => (
                        <Tr rowData={v} rowIndex={i}
                            key={`tr-${i}`} />
                    ))
                }
            </tbody>
        </table>
    )
});

export default Table;

 

 

import React, { memo } from 'react';
import Td from './Td';

const Tr = memo(({ rowData, rowIndex }) => {
    return (
        <tr>
            {rowData ?
                rowData.map((v, i) => (
                    <Td tdData={v} tr={rowIndex} td={i}
                        key={`td-${i}`} />
                ))
                : null
            }
        </tr>

    )
});

export default Tr;