Classic Minesweeper game.
---
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."
}
```