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