Classic Minesweeper game.
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 />);