Skip to content

Instantly share code, notes, and snippets.

@gaearon
Last active December 24, 2024 13:16
Show Gist options
  • Select an option

  • Save gaearon/bad24360371fcb4fe0bddc6645f6b2c6 to your computer and use it in GitHub Desktop.

Select an option

Save gaearon/bad24360371fcb4fe0bddc6645f6b2c6 to your computer and use it in GitHub Desktop.

Revisions

  1. gaearon revised this gist Dec 29, 2021. No changes.
  2. gaearon created this gist Dec 29, 2021.
    18 changes: 18 additions & 0 deletions minesweeper.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,18 @@
    <!DOCTYPE html>
    <head>
    <meta charset="UTF-8">
    </head>
    <body>
    <div id="canvas"></div>
    <button id="restart">Restart</button>
    <script src="minesweeper.js"></script>
    <style>
    * {
    font-weight: bold;
    }
    #restart {
    margin-top: 10px;
    display: none;
    }
    </style>
    </body>
    212 changes: 212 additions & 0 deletions minesweeper.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,212 @@
    let ROWS = 9
    let COLS = 9
    let SIZE = 24
    let canvas = document.getElementById('canvas')
    let restartButton = document.getElementById('restart')

    let cells

    let failedBombKey
    let revealedKeys
    let flaggedKeys
    let map

    function toKey(row, col) {
    return row + '-' + col
    }

    function fromKey(key) {
    return key.split('-').map(Number)
    }

    function createButtons() {
    canvas.style.width = ROWS * SIZE + 'px'
    canvas.style.height = COLS * SIZE + 'px'
    for (let i = 0; i < ROWS; i++) {
    for (let j = 0; j < COLS; j++) {
    let cell = document.createElement('button')
    cell.style.float = 'left'
    cell.style.width = SIZE + 'px'
    cell.style.height = SIZE + 'px'
    cell.oncontextmenu = (e) => {
    if (failedBombKey !== null) {
    return
    }
    e.preventDefault()
    toggleFlag(key)
    updateButtons();
    }
    cell.onclick = (e) => {
    if (failedBombKey !== null) {
    return
    }
    if (flaggedKeys.has(key)) {
    return
    }
    revealCell(key)
    updateButtons();
    }
    canvas.appendChild(cell)
    let key = toKey(i, j)
    cells.set(key, cell)
    }
    }
    restartButton.onclick = startGame
    }

    function startGame() {
    failedBombKey = null
    revealedKeys = new Set()
    flaggedKeys = new Set()
    map = generateMap(generateBombs())
    if (cells) {
    updateButtons()
    } else {
    cells = new Map()
    createButtons()
    }
    }

    function updateButtons() {
    for (let i = 0; i < ROWS; i++) {
    for (let j = 0; j < COLS; j++) {
    let key = toKey(i, j)
    let cell = cells.get(key)

    cell.style.backgroundColor = ''
    cell.style.color = 'black'
    cell.textContent = ''
    cell.disabled = false

    let value = map.get(key)
    if (failedBombKey !== null && value === 'bomb') {
    cell.disabled = true
    cell.textContent = '💣'
    if (key === failedBombKey) {
    cell.style.backgroundColor = 'red'
    }
    } else if (revealedKeys.has(key)) {
    cell.disabled = true
    if (value === undefined) {
    // empty
    } else if (value === 1) {
    cell.textContent = '1'
    cell.style.color = 'blue'
    } else if (value === 2) {
    cell.textContent = '2'
    cell.style.color = 'green'
    } else if (value >= 3) {
    cell.textContent = value
    cell.style.color = 'red'
    } else {
    throw Error('should never happen')
    }
    } else if (flaggedKeys.has(key)) {
    cell.textContent = '🚩'
    }
    }
    }
    if (failedBombKey !== null) {
    canvas.style.pointerEvents = 'none'
    restartButton.style.display = 'block'
    } else {
    canvas.style.pointerEvents = ''
    restartButton.style.display = ''
    }
    }

    function toggleFlag(key) {
    if (flaggedKeys.has(key)) {
    flaggedKeys.delete(key)
    } else {
    flaggedKeys.add(key)
    }
    }

    function revealCell(key) {
    if (map.get(key) === 'bomb') {
    failedBombKey = key
    } else {
    propagateReveal(key, new Set())
    }
    }

    function propagateReveal(key, visited) {
    revealedKeys.add(key)
    visited.add(key)

    let isEmpty = !map.has(key)
    if (isEmpty) {
    for (let neighborKey of getNeighbors(key)) {
    if (!visited.has(neighborKey)) {
    propagateReveal(neighborKey, visited)
    }
    }
    }
    }

    function isInBounds([row, col]) {
    if (row < 0 || col < 0) {
    return false
    }
    if (row >= ROWS || col >= COLS) {
    return false
    }
    return true
    }

    function getNeighbors(key) {
    let [row, col] = fromKey(key)
    let neighborRowCols = [
    [row - 1, col - 1],
    [row - 1, col],
    [row - 1, col + 1],
    [row, col - 1],
    [row, col + 1],
    [row + 1, col - 1],
    [row + 1, col],
    [row + 1, col + 1],
    ]
    return neighborRowCols
    .filter(isInBounds)
    .map(([r, c]) => toKey(r, c))
    }

    function generateBombs() {
    let count = Math.round(Math.sqrt(ROWS * COLS))
    let bombs = []
    let allKeys = []
    for (let i = 0; i < ROWS; i++) {
    for (let j = 0; j < COLS; j++) {
    allKeys.push(toKey(i, j))
    }
    }
    allKeys.sort(() => {
    let coinFlip = Math.random() > 0.5
    return coinFlip ? 1 : -1
    })
    return allKeys.slice(0, count)
    }

    function generateMap(seedBombs) {
    let map = new Map()
    function incrementDanger(neighborKey) {
    if (!map.has(neighborKey)) {
    map.set(neighborKey, 1);
    } else {
    let oldVal = map.get(neighborKey)
    if (oldVal !== 'bomb') {
    map.set(neighborKey, oldVal + 1)
    }
    }
    }
    for (let key of seedBombs) {
    map.set(key, 'bomb');
    for (let neighborKey of getNeighbors(key)) {
    incrementDanger(neighborKey)
    }
    }
    return map
    }

    startGame()