---
format: typebulb/v1
name: Minesweeper
---

**code.tsx**

```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 />);
```
**styles.css**

```css

html, body {
  height: 100%;
  margin: 0;
  font-family: system-ui, sans-serif;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  background-color: var(--bg);
  color: var(--fg);
}

:root {
  --bg: #f4f4f4;
  --fg: #111;
  --cell-bg: #ccc;
  --cell-border: #888;
  --revealed-bg: #fff;
}

html[data-theme="dark"] {
  --bg: #111;
  --fg: #eee;
  --cell-bg: #333;
  --cell-border: #555;
  --revealed-bg: #222;
}

.minesweeper {
  text-align: center;
  margin-top: 2rem;
}

.board {
  display: grid;
  gap: 2px;
  margin-top: 1em;
  width: max-content;
  margin-inline: auto;
}

.cell {
  width: 30px;
  height: 30px;
  background-color: var(--cell-bg);
  border: 1px solid var(--cell-border);
  display: flex;
  align-items: center;
  justify-content: center;
  user-select: none;
  cursor: pointer;
  font-weight: bold;
}

.cell.revealed {
  background-color: var(--revealed-bg);
  cursor: default;
}

.cell.flag {
  color: red;
}

.cell.mine {
  background-color: #d33;
  color: #fff;
}

.status {
  font-size: 1.2em;
  margin-bottom: 0.4em;
}

.reset {
  padding: 0.3em 0.7em;
  background: var(--cell-bg);
  border: 1px solid var(--cell-border);
  border-radius: 3px;
  color: var(--fg);
  cursor: pointer;
}

.reset:hover {
  background-color: var(--revealed-bg);
}

```
**index.html**

```html
<div id="root"></div>
```
**config.json**

```json
{
  "dependencies": {
    "react": "^19.2.3",
    "react-dom": "^19.2.3"
  },
  "ts": {
    "jsxImportSource": "react"
  },
  "description": "Classic Minesweeper game."
}
```