Last active
November 11, 2022 15:04
-
-
Save sebinsua/40b26f59b9d48e6dd363441ae1b17e93 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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