Mercurial
view react_games/src/Minesweeper/main.tsx @ 71:75de5903355c
Giagantic changes that update Dowa library to be more align with stb style array and hashmap. Updated Seobeo to be caching on server side instead of file level caching. Deleted bunch of things I don't really use.
| author | June Park <parkjune1995@gmail.com> |
|---|---|
| date | Sun, 28 Dec 2025 20:34:22 -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, }