view react_games/src/Minesweeper/main.tsx @ 279:b3b547563ec7

Add Google connector service and agent wiki Implement the C/Seobeo Google Drive and Gmail connector with encrypted OAuth storage, Zenbu authentication, browser testing, AI tool discovery, chunked HTTP decoding, and Bazel coverage. Consolidate repository guidance into progressive wiki documentation and enforce arena-first allocation for new first-party C code. Co-authored-by: Copilot <[email protected]> Copilot-Session: 84c338fd-0939-4bb3-b7f3-1062eb213e5d
author MrJuneJune <me@mrjunejune.com>
date Mon, 17 Aug 2026 22:22:36 -0700
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,
}