Skip to content

Instantly share code, notes, and snippets.

@sebinsua
Last active November 11, 2022 15:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sebinsua/40b26f59b9d48e6dd363441ae1b17e93 to your computer and use it in GitHub Desktop.
Save sebinsua/40b26f59b9d48e6dd363441ae1b17e93 to your computer and use it in GitHub Desktop.
import { useCallback, useState } from "react";
import "./styles.css";
const EMPTY = false;
const MINE = true;
const MINE_LIKELIHOOD_THRESHOLD = 0.9;
interface Cell {
mine: boolean;
excavated: boolean;
}
function createBoard(size: number): Cell[][] {
const board = Array(size)
.fill(() =>
Array(size)
.fill(EMPTY)
.map(() => ({
mine: Math.random() > MINE_LIKELIHOOD_THRESHOLD ? MINE : EMPTY,
excavated: false
}))
)
.map((createRow) => createRow());
return board;
}
function getNearbyMineCount(y: number, x: number, board: Cell[][]) {
const height = board.length;
const width = board[0].length;
const hasMine = (y: number, x: number, board: Cell[][]) =>
y >= 0 && y < height && x >= 0 && x < width && board[y][x].mine;
return [
hasMine(y - 1, x - 1, board),
hasMine(y - 1, x, board),
hasMine(y - 1, x + 1, board),
hasMine(y, x + 1, board),
hasMine(y + 1, x + 1, board),
hasMine(y + 1, x, board),
hasMine(y + 1, x - 1, board),
hasMine(y, x - 1, board)
].filter(Boolean).length;
}
function canExcavate(y: number, x: number, board: Cell[][]) {
const height = board.length;
const width = board[0].length;
return (
y >= 0 &&
y < height &&
x >= 0 &&
x < width &&
board[y][x].mine === false &&
board[y][x].excavated === false
);
}
function excavate(y: number, x: number, board: Cell[][]) {
return board.map((row, _y) =>
row.map((c, _x) => (y === _y && x === _x ? { ...c, excavated: true } : c))
);
}
function excavateAll(board: Cell[][]) {
return board.map((row) => row.map((cell) => ({ ...cell, excavated: true })));
}
function MineSweeper() {
const [board, setBoard] = useState(() => createBoard(10));
// TODO:
// - Create a button to restart the game.
// - Show number of moves.
// - Once the first square is revealed start a timer to keep track
// of how long the game takes to be finished.
// - Game is lost if a mine was clicked on (excavated but not flagged).
// - Game is won if the whole board is excavated apart from the squares containing mines.
// - Right-click to drop a flag plus excavate. Only allow M flags, where M is the number of mines.
const handleClick = useCallback((event) => {
const y = parseInt(event.currentTarget.dataset.y, 10);
const x = parseInt(event.currentTarget.dataset.x, 10);
setBoard((board) => {
const currentCell = board[y][x];
if (currentCell.mine) {
return excavateAll(board);
}
const nearbyMines = getNearbyMineCount(y, x, board);
if (nearbyMines > 0) {
return excavate(y, x, board);
}
const toExcavate = [[y, x]];
let newBoard = board;
while (toExcavate.length) {
const [y, x] = toExcavate.pop()!;
newBoard = excavate(y, x, newBoard);
if (getNearbyMineCount(y, x, newBoard) > 0) {
continue;
}
const preparedExcavations = [
canExcavate(y - 1, x - 1, newBoard) ? [y - 1, x - 1] : undefined,
canExcavate(y - 1, x, newBoard) ? [y - 1, x] : undefined,
canExcavate(y - 1, x + 1, newBoard) ? [y - 1, x + 1] : undefined,
canExcavate(y, x + 1, newBoard) ? [y, x + 1] : undefined,
canExcavate(y + 1, x + 1, newBoard) ? [y + 1, x + 1] : undefined,
canExcavate(y + 1, x, newBoard) ? [y + 1, x] : undefined,
canExcavate(y + 1, x - 1, newBoard) ? [y + 1, x - 1] : undefined,
canExcavate(y, x - 1, newBoard) ? [y, x - 1] : undefined
].filter(
(excavation: number[] | undefined): excavation is [number, number] =>
excavation !== undefined
);
if (preparedExcavations.length > 0) {
toExcavate.push(...preparedExcavations);
}
}
return newBoard;
});
}, []);
const renderCell = (y: number, x: number, board: Cell[][]) => {
const cell = board[y][x];
if (!cell.excavated) {
return "";
}
if (cell.mine) {
return "💣";
}
const nearbyMines = getNearbyMineCount(y, x, board);
return nearbyMines > 0 ? `${nearbyMines}` : "";
};
return (
<div className="grid">
{board.map((row, y) => (
<div key={`y:${y}`} className="row">
{row.map((_, x) => (
<div
key={`y:${y}-x:${x}`}
data-y={y}
data-x={x}
className={[
"cell",
board[y][x].excavated ? "excavated" : "unexcavated"
]
.filter(Boolean)
.join(" ")}
onClick={handleClick}
>
{renderCell(y, x, board)}
</div>
))}
</div>
))}
</div>
);
}
function App() {
return (
<div className="App">
<MineSweeper />
</div>
);
}
export default App;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment