Minesweeper

Classic Minesweeper game.

code.tsx

import React from "react";
import { createRoot } from "react-dom/client";

type Cell = {
  revealed: boolean;
  mine: boolean;
  flag: boolean;
  adjacent: number;
};

type Board = Cell[][];

const directions = [
  [-1, -1], [-1, 0], [-1, 1],
  [0, -1], [0, 1],
  [1, -1], [1, 0], [1, 1],
] as const;

function generateBoard(rows: number, cols: number, mines: number): Board {
  const board: Board = Array.from({ length: rows }, () =>
    Array.from({ length: cols }, () => ({
      revealed: false,
      mine: false,
      flag: false,
      adjacent: 0,
    }))
  );

  // place mines randomly
  for (let placed = 0; placed < mines; ) {
    const r = Math.floor(Math.random() * rows);
    const c = Math.floor(Math.random() * cols);
    if (!board[r][c].mine) {
      board[r][c].mine = true;
      placed++;
    }
  }

  // count adjacency
  for (let r = 0; r < rows; r++) {
    for (let c = 0; c < cols; c++) {
      if (board[r][c].mine) continue;
      board[r][c].adjacent = directions.filter(([dr, dc]) => 
        board[r + dr]?.[c + dc]?.mine
      ).length;
    }
  }
  return board;
}

function App() {
  const rows = 10, cols = 10, mines = 15;
  const [board, setBoard] = React.useState(() => generateBoard(rows, cols, mines));
  const [gameOver, setGameOver] = React.useState(false);
  const [win, setWin] = React.useState(false);

  const revealCell = (r: number, c: number, grid: Board): void => {
    const cell = grid[r]?.[c];
    if (!cell || cell.revealed || cell.flag) return;
    cell.revealed = true;
    if (!cell.mine && cell.adjacent === 0) {
      directions.forEach(([dr, dc]) => revealCell(r + dr, c + dc, grid));
    }
  };

  const handleLeftClick = (r: number, c: number) => {
    if (gameOver || board[r][c].flag || board[r][c].revealed) return;
    setBoard(prev => {
      const next = prev.map(row => row.map(cell => ({ ...cell })));
      if (next[r][c].mine) {
        next.forEach(row => row.forEach(cell => (cell.revealed = true)));
        setGameOver(true);
        setWin(false);
      } else {
        revealCell(r, c, next);
        if (next.every(row => row.every(cell => cell.mine || cell.revealed))) {
        setGameOver(true);
        setWin(true);
      }
      }
      return next;
    });
  };

  const handleRightClick = (e: React.MouseEvent, r: number, c: number) => {
    e.preventDefault();
    if (gameOver || board[r][c].revealed) return;
    setBoard(prev => {
      const next = prev.map(row => row.map(cell => ({ ...cell })));
      next[r][c].flag = !next[r][c].flag;
      return next;
    });
  };

  const reset = () => {
    setBoard(generateBoard(rows, cols, mines));
    setGameOver(false);
    setWin(false);
  };

  return (
    <div className="minesweeper">
      <div className="status">
        {gameOver ? (win ? "🎉 You win!" : "💥 Game over!") : "🙂 Playing..."}
      </div>
      <button className="reset" onClick={reset}>Reset</button>
      <div className="board" style={{ gridTemplateColumns: `repeat(${cols}, 1fr)` }}>
        {board.map((row, r) =>
          row.map((cell, c) => (
            <div
              key={`${r}-${c}`}
              className={`cell ${cell.revealed ? "revealed" : ""} ${cell.flag ? "flag" : ""} ${cell.mine && gameOver ? "mine" : ""}`}
              onClick={() => handleLeftClick(r, c)}
              onContextMenu={(e) => handleRightClick(e, r, c)}
            >
              {cell.revealed && cell.mine ? "💣"
                : cell.revealed && cell.adjacent > 0 ? cell.adjacent
                : cell.flag
                ? "🚩"
                : ""}
            </div>
          ))
        )}
      </div>
    </div>
  );
}

const container = document.getElementById("root");
const root = createRoot(container!);
root.render(<App />);

Markdown source · More bulbs by samples · Typebulb home