introduce

Minesweeper is a popular puzzle game, the goal of the game is to find out all the non-mine grid according to the number of clicking on the grid in the shortest time, while avoiding stepping on a mine, stepping on a mine is to lose everything

So under the basic rules of the game, a similar version of the game was hand-stroked in native JavaScript.

Game design

  • Use Canvas as the basis for game design
  • Draw an area of 20 x 30 cells
  • For the random generation of landmines, the generation method of random numbers is adopted, and the generation probability is 20%
  • To start the game, choose a blank area at random as the starting point
  • Mouse event handling: left click to open cells, right click to mark cells, and right click again to unmark cells.
  • Marked cells cannot be left open and must be unmarked first
  • The game ends when the left click hits the mine, and then the end processing is performed:
    • All unmarked mines are displayed
    • Re-mark all incorrectly marked cells with a red X
    • Correct mine markings are not processed
  • The game ends when all non-mine cells have been clicked

One thing the current design doesn’t do, and of course it doesn’t affect the game design, is timings, double clicks, and landmine counts.

Thinking on

Create the template first: determine the necessary constants
<div class="main">
  <h3>Mine clearance<span class="restart" onclick="mineSweeper.restart()">Start all over again</span></h3>
  <div class="container">
    <div class="mask"></div>
    <canvas id="canvas"></canvas>
  </div>
</div>

<style>
  .main {
    width: 1000px;
    margin: 0 auto;
  }
  .restart {
    cursor: pointer;
    font-size: 16px;
    margin-left: 20px;
  }
  .container {
    width: 752px;
    padding: 20px;
    background-color: #ccc;
    border-radius: 5px;
    position: relative;
  }
  .mask {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 100;
    display: none;
  }
  #canvas {
    background-color: #bbb;
  }
</style>
<script>
  class MineSweeper {
    constructor() {
      this.canvas = document.querySelector('#canvas')
      this.mask = document.querySelector('.mask')
      this.ctx = canvas.getContext('2d')

      this.row = 20 / / the number of rows
      this.col = 30 / / the number of columns
      this.cellWidth = 25  // Cell width
      this.cellHeight = 25 // Cell height

      this.width = this.col * this.cellWidth
      this.height = this.row * this.cellHeight

      this.canvas.width = this.width
      this.canvas.height = this.height
      // Use different colors for different numbers
      this.colors = [
        '#FF7F00'.'#00FF00'.'#FF0000'.'#00FFFF'.'#0000FF'.'#8B00FF'.'#297b83'.'#0b0733'
      ]
      this.mineTotal = 0 // Total number of landmines
      this.cellTotal = this.row * this.col // Total number of cells
      // Direction array
      this.direction = [
        -this.col,      / /
        this.col,       / /
        -1./ / left
        1./ / right
        -this.col - 1./ / left
        this.col - 1./ / lower left
        -this.col + 1./ / right
        this.col + 1    / / right]}}new MineSweeper()
</script>
Copy the code
Draw grid lines
drawLine() {
  const { ctx, row, col, width, height, cellWidth, cellHeight } = this
  for (let i = 0; i <= row; i++) {
    ctx.moveTo(0, i * cellWidth)
    ctx.lineTo(width, i * cellWidth)
  }
  for (let i = 0; i <= col; i++) {
    ctx.moveTo(i * cellHeight, 0)
    ctx.lineTo(i * cellHeight, height)
  }
  ctx.lineWidth = 3
  ctx.strokeStyle = '#ddd'
  ctx.stroke()
}
Copy the code

At this point, the initial template is out, simple! Here is the generated data:

Generate mine data

Generate a row * col length array containing mine data and count the number of mines around each non-mine cell. Mine-generated colleagues need a state array (MARK) to record the state of each cell (opened, not opened, marked)

restart() {
  this.mineTotal = 0
  this.cellTotal = this.row * this.col
  this.mask.style.display = 'none'
  // -1 is a mine
  this.datas = new Array(this.cellTotal).fill(0).map(v= > {
    if (Math.random() > 0.8) {
      this.mineTotal++
      return -1
    }
    return 0
  })
  this.mark = new Array(this.cellTotal).fill(0) // 0 indicates unopened, 1 indicates opened, and 2 indicates marked
  this.calcRound()
}
// Calculate the prompt number near the mine
calcRound() {
  const { datas, direction } = this
  
  for (let i = 0; i < datas.length; i++) {
    if(datas[i] ! = = -1) {
      for (let d of direction) {
        const newIndex = i + d
        // boundary judgment
        if (this.isCross(i, newIndex, d, this.col, datas.length)) continue

        if (datas[newIndex] === -1) {
          datas[i]++
        }
      }
    }
  }
}
Copy the code
Calculate mouse position

We need to get the index subscript of the array when we click inside the Canvas element

Define mouse clicks and right clicks in the constructor method
constructor() {
  // ...
  this.restart()

  // Listen for click events
  this.canvas.addEventListener('click'.(e) = > {
    const cellIndex = this.calcMouseCell(e)
    const cellValue = this.datas[cellIndex]
    // Both marked and marked cells cannot be clicked
    if (this.mark[cellIndex] === 0) {
      if (cellValue === -1) {
        this.gameOver(cellIndex)
      } else {
        // If the value of the current cell is 0, then the surrounding 8 directions are not opened, and recurse
        if (cellValue === 0) {
          this.openCell(cellIndex)
        } else {
          this.mark[cellIndex] = 1
          this.cellTotal--
        }
        this.draw()
      }
    }
  })

  // Custom canvas right-click to mark and unmark
  this.canvas.oncontextmenu = (e) = > {
    if (e.button === 2) {
      const cellIndex = this.calcMouseCell(e)
      // Check if it has been clicked
      if (this.mark[cellIndex] === 1) {
        return false
      }
      if (this.mark[cellIndex] === 0) {
        this.mark[cellIndex] = 2
      } else {
        this.mark[cellIndex] = 0
      }
      this.draw()
    }
    return false}}// Get the click position based on mouse events
calcMouseCell(e) {
  const row = e.offsetY / this.cellHeight | 0
  const col = e.offsetX / this.cellWidth | 0
  return row * this.col + col
}
Copy the code

When the point to the cell is 0, the surrounding cell must be mine free, do automatic open, and redraw

// Recursively opens adjacent cells of the cell with the number 0
openCell(cellIndex) {
  const { datas, mark, direction } = this
  mark[cellIndex] = 1
  this.cellTotal--

  for (let d of direction) {
    const newIndex = cellIndex + d
    // Determine if the boundary is crossed
    if (
      this.isCross(cellIndex, newIndex, d, this.col, datas.length) || mark[newIndex] ! = =0
    ) continue

    if (datas[newIndex] === 0) {
      this.openCell(newIndex)
    } else {
      mark[newIndex] = 1
      this.cellTotal--
    }
  }
}
// Determine cell boundaries
isCross(oriIndex, newIndex, d, col, maxLength) {
  if (
    newIndex < 0 || / / on the border
    newIndex >= maxLength || / / lower boundary
    (oriIndex % col === 0 && (d === -1 || d === -31 || d === 29)) || / / the left border
    (oriIndex % col === col - 1 && (d === 1 || d === -29 || d === 31)) / / right border
  ) {
    return true
  }
  return false
}
Copy the code

Auto-expand around the 0 cell is done, so we can add auto-expand to the start method by randomly selecting a blank area at the start:

restart() {
  this.mineTotal = 0
  this.cellTotal = this.row * this.col
  this.mask.style.display = 'none'
  // -1 is a mine
  this.datas = new Array(this.cellTotal).fill(0).map(v= > {
    if (Math.random() > 0.8) {
      this.mineTotal++
      return -1
    }
    return 0
  })
  this.mark = new Array(this.cellTotal).fill(0) // 0 indicates unopened, 1 indicates opened, and 2 indicates marked
  this.calcRound()

  // Select a random 0 position to automatically open as the starting position
  let randomIndex = Math.random() * this.datas.length | 0
  while(this.datas[randomIndex] ! = =0) {
    randomIndex = Math.random() * this.datas.length | 0
  }
  this.openCell(randomIndex)
  
  this.draw()
}
Copy the code

Draw method: Loop through the array containing mine data. Since the array is a one-dimensional array and the region drawn is a two-dimensional array format, we need to use the values of cellWidth and cellHeight for coordinate conversion. Drawing cells requires combining state arrays to draw in different cases:

  • When mark[I] === 0, no processing is done
  • When mark[I] === 1, it indicates that the current cell has been opened, the background color of the current cell needs to be changed, and the number that is not 0 needs to be drawn
  • When mark[I] === 2, it means that the current cell is marked. To take the place of
// Draw mine data
drawMine() {
  const { datas, ctx, mark, cellWidth, cellHeight, col } = this
  ctx.font = 'Bold 18px "Microsoft Yahei"
  ctx.textAlign = 'center'
  ctx.textBaseline = 'top'
  for (let i = 0; i < datas.length; i++) {
    if (mark[i] === 0) continue

    const rowIndex = (i / col | 0) * cellWidth
    const colIndex = i % col * cellHeight
    if (mark[i] === 1) {
      ctx.fillStyle = '#eee'
      ctx.fillRect(colIndex + 2, rowIndex + 2, cellWidth - 4, cellHeight - 4)
      // Display numbers unless 0
      if (datas[i]) {
        ctx.fillStyle = this.colors[datas[i] - 1]
        ctx.fillText(datas[i], colIndex + 12, rowIndex + 6)}}else { / / tag
      ctx.fillStyle = '# 000'
      ctx.fillText('? ', colIndex + 12, rowIndex + 6)}}}Copy the code

Because the game’s drawing, data and grid lines need to be redrawn together, a unified approach is adopted

/ / to draw
draw() {
  this.canvas.height = this.height // Empty the canvas
  this.drawLine()
  this.drawMine()
}
Copy the code
Game over

1. When a mine is clicked, the game ends

/** * End of the game to display all mines * 1, correct marks are not moved * 2, wrong marks are marked with red X * 3, unmarked mines are displayed ** 4, the last click of the mine cell marked with red background */
gameOver(lastCell) {
  this.canvas.height = this.height
  this.drawLine()
  const { datas, ctx, mark, cellWidth, cellHeight, col } = this

  ctx.font = 'Bold 18px "Microsoft Yahei"
  ctx.textAlign = 'center'
  ctx.textBaseline = 'top'
  
  for (let i = 0; i < datas.length; i++) {
    const rowIndex = (i / col | 0) * cellWidth
    const colIndex = i % col * cellHeight

    if (mark[i] === 1) {
      ctx.fillStyle = '#eee'
      ctx.fillRect(colIndex + 2, rowIndex + 2, cellWidth - 4, cellHeight - 4)
      if (datas[i]) {
        ctx.fillStyle = this.colors[datas[i] - 1]
        ctx.fillText(datas[i], colIndex + 12, rowIndex + 6)}}else {
      if (datas[i] === -1) {
        ctx.fillStyle = '# 000'
        if (mark[i] === 2) {
          ctx.fillText('? ', colIndex + 12, rowIndex + 6)}else {
          if (i === lastCell) {
            ctx.fillStyle = 'red'
            ctx.fillRect(colIndex + 2, rowIndex + 2, cellWidth - 4, cellHeight - 4)
            ctx.fillStyle = '# 000'
            ctx.fillText(The '*', colIndex + 13, rowIndex + 9)}else {
            ctx.fillText(The '*', colIndex + 13, rowIndex + 9)}}}else if (mark[i] === 2) {
        ctx.fillStyle = 'red'
        ctx.fillText('x', colIndex + 12, rowIndex + 4)}}}this.mask.style.display = 'block'
}
Copy the code

2. The game is complete when all non-mine squares are open

// Whether to complete
isComplete() {
  // The remaining untapped cells are equal to the total number of mines
  return this.cellTotal === this.mineTotal
}

// Load the judgment at the end of the draw completion method
draw() {
  this.canvas.height = this.height
  this.drawLine()
  this.drawMine()

  if (this.isComplete()) {
    // setTimeout is used to wait for the Canvas to finish rendering
    setTimeout(() = > {
      this.mask.style.display = 'block'
      alert('Game complete')}}}Copy the code

Here the main function of mine clearance is completed, the amount of code is not very much, the total code is only a little more than 300.

Github.com/554246839/t…

The complete code

<! DOCTYPEhtml>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <title>Mine clearance</title>
  <style>
    .main {
      width: 1000px;
      margin: 0 auto;
    }
    .restart {
      cursor: pointer;
      font-size: 16px;
      margin-left: 20px;
    }
    .container {
      width: 752px;
      padding: 20px;
      background-color: #ccc;
      border-radius: 5px;
      position: relative;
    }
    .mask {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      z-index: 100;
      display: none;
    }
    #canvas {
      background-color: #bbb;
    }
  </style>
</head>
<body>
  <div class="main">
    <h3>Mine clearance<span class="restart" onclick="mineSweeper.restart()">Start all over again</span></h3>
    <div class="container">
      <div class="mask"></div>
      <canvas id="canvas"></canvas>
    </div>
  </div>

  <script>
    class MineSweeper {
      constructor() {
        this.canvas = document.querySelector('#canvas')
        this.mask = document.querySelector('.mask')
        this.ctx = canvas.getContext('2d')

        this.row = 20
        this.col = 30
        this.cellWidth = 25
        this.cellHeight = 25

        this.width = this.col * this.cellWidth
        this.height = this.row * this.cellHeight

        this.canvas.width = this.width
        this.canvas.height = this.height
        // Use different colors for different numbers
        this.colors = [
          '#FF7F00'.'#00FF00'.'#FF0000'.'#00FFFF'.'#0000FF'.'#8B00FF'.'#297b83'.'#0b0733'
        ]
        this.mineTotal = 0 // Total number of landmines
        this.cellTotal = this.row * this.col // Total number of cells
        // Direction array
        this.direction = [
          -this.col,      / /
          this.col,       / /
          -1./ / left
          1./ / right
          -this.col - 1./ / left
          this.col - 1./ / lower left
          -this.col + 1./ / right
          this.col + 1    / / right
        ]

        this.restart()

        // Listen for click events
        this.canvas.addEventListener('click'.(e) = > {
          const cellIndex = this.calcMouseCell(e)
          const cellValue = this.datas[cellIndex]
          // Both marked and marked cells cannot be clicked
          if (this.mark[cellIndex] === 0) {
            if (cellValue === -1) {
              this.gameOver(cellIndex)
            } else {
              // If the value of the current cell is 0, then the surrounding 8 directions are not opened, and recurse
              if (cellValue === 0) {
                this.openCell(cellIndex)
              } else {
                this.mark[cellIndex] = 1
                this.cellTotal--
              }
              this.draw()
            }
          }
        })

        // Custom canvas right-click to mark and unmark
        this.canvas.oncontextmenu = (e) = > {
          if (e.button === 2) {
            const cellIndex = this.calcMouseCell(e)
            // Check if it has been clicked
            if (this.mark[cellIndex] === 1) {
              return false
            }
            if (this.mark[cellIndex] === 0) {
              this.mark[cellIndex] = 2
            } else {
              this.mark[cellIndex] = 0
            }
            this.draw()
          }
          return false}}restart() {
        this.mineTotal = 0
        this.cellTotal = this.row * this.col
        this.mask.style.display = 'none'
        // -1 is a mine
        this.datas = new Array(this.cellTotal).fill(0).map(v= > {
          if (Math.random() > 0.8) {
            this.mineTotal++
            return -1
          }
          return 0
        })
        this.mark = new Array(this.cellTotal).fill(0) // 0 indicates unopened, 1 indicates opened, and 2 indicates marked
        this.calcRound()

        // Select a random 0 position to automatically open as the starting position
        let randomIndex = Math.random() * this.datas.length | 0
        while(this.datas[randomIndex] ! = =0) {
          randomIndex = Math.random() * this.datas.length | 0
        }
        this.openCell(randomIndex)
        
        this.draw()
      }

      // Recursively opens adjacent cells of the cell with the number 0
      openCell(cellIndex) {
        const { datas, mark, direction } = this
        mark[cellIndex] = 1
        this.cellTotal--

        for (let d of direction) {
          const newIndex = cellIndex + d
          if (this.isCross(cellIndex, newIndex, d, this.col, datas.length) || mark[newIndex] ! = =0) continue

          if (datas[newIndex] === 0) {
            this.openCell(newIndex)
          } else {
            mark[newIndex] = 1
            this.cellTotal--
          }
        }
      }

      // Get the click position based on mouse events
      calcMouseCell(e) {
        const row = e.offsetY / this.cellHeight | 0
        const col = e.offsetX / this.cellWidth | 0
        return row * this.col + col
      }

      // Calculate the prompt number near the mine
      calcRound() {
        const { datas, direction } = this
        
        for (let i = 0; i < datas.length; i++) {
          if(datas[i] ! = = -1) {
            for (let d of direction) {
              const newIndex = i + d
              // boundary judgment
              if (this.isCross(i, newIndex, d, this.col, datas.length)) continue

              if (datas[newIndex] === -1) {
                datas[i]++
              }
            }
          }
        }
      }

      // Determine cell boundaries
      isCross(oriIndex, newIndex, d, col, maxLength) {
        if (
          newIndex < 0 || / / on the border
          newIndex >= maxLength || / / lower boundary
          (oriIndex % col === 0 && (d === -1 || d === -31 || d === 29)) || / / the left border
          (oriIndex % col === col - 1 && (d === 1 || d === -29 || d === 31)) / / right border
        ) {
          return true
        }
        return false
      }

      / / to draw
      draw() {
        this.canvas.height = this.height
        this.drawLine()
        this.drawMine()

        if (this.isComplete()) {
          setTimeout(() = > {
            this.mask.style.display = 'block'
            alert('Game complete')}}}// Draw mine data
      drawMine() {
        const { datas, ctx, mark, cellWidth, cellHeight, col } = this
        ctx.font = 'Bold 18px "Microsoft Yahei"
        ctx.textAlign = 'center'
        ctx.textBaseline = 'top'
        for (let i = 0; i < datas.length; i++) {
          if (mark[i] === 0) continue

          const rowIndex = (i / col | 0) * cellWidth
          const colIndex = i % col * cellHeight

          if (mark[i] === 1) {
            ctx.fillStyle = '#eee'
            ctx.fillRect(colIndex + 2, rowIndex + 2, cellWidth - 4, cellHeight - 4)
            // Display numbers unless 0
            if (datas[i]) {
              ctx.fillStyle = this.colors[datas[i] - 1]
              ctx.fillText(datas[i], colIndex + 12, rowIndex + 6)}}else {
            ctx.fillStyle = '# 000'
            ctx.fillText('? ', colIndex + 12, rowIndex + 6)}}}// Draw grid lines
      drawLine() {
        const { ctx, row, col, width, height, cellWidth, cellHeight } = this
        for (let i = 0; i <= row; i++) {
          ctx.moveTo(0, i * cellWidth)
          ctx.lineTo(width, i * cellWidth)
        }
        for (let i = 0; i <= col; i++) {
          ctx.moveTo(i * cellHeight, 0)
          ctx.lineTo(i * cellHeight, height)
        }
        ctx.lineWidth = 3
        ctx.strokeStyle = '#ddd'
        ctx.stroke()
      }
      
      // Whether to complete
      isComplete() {
        return this.cellTotal === this.mineTotal
      }

      /** * End of the game to display all mines * 1, correct marks are not moved * 2, wrong marks are marked with red X * 3, unmarked mines are displayed ** 4, the last click of the mine cell marked with red background */
      gameOver(lastCell) {
        this.canvas.height = this.height
        this.drawLine()
        const { datas, ctx, mark, cellWidth, cellHeight, col } = this

        ctx.font = 'Bold 18px "Microsoft Yahei"
        ctx.textAlign = 'center'
        ctx.textBaseline = 'top'
        
        for (let i = 0; i < datas.length; i++) {
          const rowIndex = (i / col | 0) * cellWidth
          const colIndex = i % col * cellHeight

          if (mark[i] === 1) {
            ctx.fillStyle = '#eee'
            ctx.fillRect(colIndex + 2, rowIndex + 2, cellWidth - 4, cellHeight - 4)
            if (datas[i]) {
              ctx.fillStyle = this.colors[datas[i] - 1]
              ctx.fillText(datas[i], colIndex + 12, rowIndex + 6)}}else {
            if (datas[i] === -1) {
              ctx.fillStyle = '# 000'
              if (mark[i] === 2) {
                ctx.fillText('? ', colIndex + 12, rowIndex + 6)}else {
                if (i === lastCell) {
                  ctx.fillStyle = 'red'
                  ctx.fillRect(colIndex + 2, rowIndex + 2, cellWidth - 4, cellHeight - 4)
                  ctx.fillStyle = '# 000'
                  ctx.fillText(The '*', colIndex + 13, rowIndex + 9)}else {
                  ctx.fillText(The '*', colIndex + 13, rowIndex + 9)}}}else if (mark[i] === 2) {
              ctx.fillStyle = 'red'
              ctx.fillText('x', colIndex + 12, rowIndex + 4)}}}this.mask.style.display = 'block'}}var mineSweeper = new MineSweeper()
  </script>
</body>
</html>
Copy the code