Mercurial
view react_games/src/Minesweeper/main.tsx @ 53:82d1fe4d4ee6
[PostDog] Postman but for dogs.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Fri, 19 Dec 2025 13:58:37 -0800 |
| parents | fb9bcd3145cb |
| children |
line wrap: on
line source
import { CSSProperties, memo, useCallback, useReducer } from 'react'; /** * * Build a simplified Minesweeper game in React where the user can click cells to reveal them. * Some cells contain mines, others show the number of adjacent mines. * If a cell with zero adjacent mines is revealed, all connected zero-cells should be revealed * automatically. * The game ends if a mine is revealed, and the player wins if all non-mine cells are revealed. * * The board should be 10×10. * * Clicking a cell reveals it: * * If it’s a mine, the game ends with a “Game Over” message. * * If it’s not a mine, show the number of adjacent mines (0–8). * * If the number is 0, automatically reveal all connected empty cells and their numbered border. * * The player wins when all non-mine cells are revealed. * * Include a Reset button to restart the game with a new random layout. * * Optional: Allow right-clicking a cell to toggle a flag icon (to mark suspected mines). */ enum CellValue { EMPTY, BOMB, } interface Cell { value: CellValue; numberOfAdjacentBomb: number; isShown: boolean; isFlag: boolean, } type Board = Cell[][]; enum GameStatus { PLAYABLE="Reveal all the map while not clicking on mine", WON="YOU WON", LOST="YOU LOST" } interface GameState { board: Board; status: GameStatus; } const MAX_SIZE_LEN = 10; const N_BOMBS = 10; const DIRECTION = [ [0,1], [1,1], [1,0], [1,-1], [0,-1], [-1,-1], [-1,0], [-1,1], ] const constructGameState = (): GameState => { let board: Board = Array.from( { length: MAX_SIZE_LEN }, () => Array.from( { length: MAX_SIZE_LEN }, (): Cell => ( { value: CellValue.EMPTY, numberOfAdjacentBomb: 0, isShown: false , isFlag: false, }) ) ); // Place the bombs let placedBomb = 0; const bombPlacments: number[][] = []; while (placedBomb < N_BOMBS) { const bombRowIdx = Math.floor(Math.random() * MAX_SIZE_LEN); const bombColIdx = Math.floor(Math.random() * MAX_SIZE_LEN); if (board[bombRowIdx][bombColIdx].value === CellValue.EMPTY) { board[bombRowIdx][bombColIdx] = { ...board[bombRowIdx][bombColIdx], value: CellValue.BOMB, } placedBomb++; bombPlacments.push([bombRowIdx, bombColIdx]); } } // Find all adjacent cells and add values for (const [bombR, bombC] of bombPlacments) { for (const [dr, dc] of DIRECTION) { if (board[bombR+dr]?.[bombC+dc]?.value === CellValue.EMPTY) { board[bombR+dr][bombC+dc] = { ...board[bombR+dr][bombC+dc], numberOfAdjacentBomb: board[bombR+dr][bombC+dc].numberOfAdjacentBomb + 1 } } } } return { board, status: GameStatus.PLAYABLE, } } enum GameActionEnum { CHECK, RESET, FLAG, } interface Position { row: number; col: number; } type GameAction = | { type: GameActionEnum.CHECK, position: Position } | { type: GameActionEnum.FLAG, position: Position } | { type: GameActionEnum.RESET } const gameStateReducer = (state: GameState, action: GameAction): GameState => { switch(action.type) { case GameActionEnum.CHECK: const {position} = action; const board = state.board.map(row=>row.map(col=>({...col}))); const start = board[position.row][position.col]; if (start.value === CellValue.BOMB) { for (const row of board) for (const cell of row) cell.isShown = true; return { board, status: GameStatus.LOST }; } if (!start.isShown) start.isShown = true; if (start.numberOfAdjacentBomb === 0) { const q: Position[] = [position]; while (q.length) { const { row, col } = q.shift()!; for (const [dr, dc] of DIRECTION) { const r = row + dr, c = col + dc; const n = board[r]?.[c]; if (!n || n.isShown || n.value === CellValue.BOMB) continue; n.isShown = true; if (n.numberOfAdjacentBomb === 0) q.push({ row: r, col: c }); } } } const hidden = board.flat().filter(c => !c.isShown).length; const status = hidden === N_BOMBS ? GameStatus.WON : GameStatus.PLAYABLE; return { board, status }; case GameActionEnum.FLAG: const newBoard = state.board.map( (row, rowIdx) => rowIdx === action.position.row ? row.map( (col, colIdx): Cell => colIdx === action.position.col ? ({...col, isFlag: true }) : col ) : row ); console.log(newBoard); return { ...state, board: newBoard, } case GameActionEnum.RESET: return constructGameState(); } } interface StylesProp { board: CSSProperties; cell: CSSProperties; } const styles: StylesProp = { board: { display: "grid", gridTemplateColumns: `repeat(${MAX_SIZE_LEN}, 1fr)`, width: 400, }, cell: { display: "flex", justifyContent: "center", alignItems: "center", width: "100%", aspectRatio: "1 / 1", border: "1px solid black", backgroundColor: "#e6e6e6" } } interface CellComponentProp { cellValue: Cell; row: number; col: number; placeDispatch: (row: number, col: number) => void; flagDispatch: (row: number, col: number) => void; } const CellComponent = memo(({ cellValue, row, col, placeDispatch, flagDispatch }: CellComponentProp) => { console.log("re-render"); const handleOnClikck = useCallback(() => { placeDispatch(row, col); }, [row, col, placeDispatch]) const handleOnRightClikck = useCallback((event: React.MouseEvent<HTMLDivElement>) => { event.preventDefault(); flagDispatch(row, col); }, [row, col, placeDispatch]) return ( <div style={{ ...styles.cell, cursor: "pointer", backgroundColor: cellValue.isShown ? "#fafafa" : "#e6e6e6", }} onClick={handleOnClikck} onContextMenu={handleOnRightClikck} > {cellValue.isShown ? cellValue.value === CellValue.BOMB ? "💣" : cellValue.numberOfAdjacentBomb || "" : cellValue.isFlag ? "🚩" : ""} </div> ) }, (prev, next) => prev.cellValue.isShown === next.cellValue.isShown && prev.cellValue.isFlag === next.cellValue.isFlag) const Minesweeper = () => { const [state, dispatch] = useReducer(gameStateReducer, null, constructGameState); const placeDispatch = useCallback( (row: number, col: number) => { dispatch({type: GameActionEnum.CHECK, position: { row, col }}) }, [dispatch]) const flagDispatch = useCallback( (row: number, col: number) => { dispatch({type: GameActionEnum.FLAG, position: { row, col }}) }, [dispatch]) return ( <> <h1> {state.status} </h1> <div style={styles.board}> {state.board.map( (row, rowIdx) => row.map( (cell, colIdx) => ( <CellComponent key={`${rowIdx}-${colIdx}`} cellValue={cell} row={rowIdx} col={colIdx} placeDispatch={placeDispatch} flagDispatch={flagDispatch} />) ) )} </div> <button onClick={()=>{dispatch({type: GameActionEnum.RESET})}}> reset </button> </> ) } export { Minesweeper, }