- How To Build Minesweeper With JavaScript
- Mitchum
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: ZavierTang
- Proofreader: Stevens1995, githubmnume
In my last post, I introduced you to a game of backgammon written in JavaScript, and before that I wrote a matching game. This week, I decided to add some complexity. You will learn how to write minesweeper games in JavaScript. I used jQuery, a JavaScript library that helps you interact with HTML. When you see a function call with a leading dollar ($) sign, this is what jQuery does. If you want to learn more about jQuery, read the official documentation.
Click to try minesweeper! The game is recommended to play on a desktop computer because it’s easier to play.
Here are the three files needed to create the game:
- HTML
- CSS
- JavaScript
If you want to learn how to write minesweeper in JavaScript, the first step is to understand how the game works. Let’s start with the rules of the game.
The rules of the game
- The minesweeper panel is a 10 x 10 square. We can set it to another size, such as the classic Windows version, but for demonstration purposes we will use the smaller “entry-level” version.
- There are a fixed number of randomly placed mines on the panel that the player cannot see.
- Each cell is in one of two states: open or closed. Clicking on a cell opens it. If there are mines lurking, the game will end in failure. If there are no mines in a cell, but there are mines in one or more adjacent cells, the open cell shows the number of mines in adjacent cells. When none of the adjacent cells are mines, they open automatically.
- Right-click a cell to mark it with a small flag. Flags indicate that the player knows that mines are lurking there.
- Holding down the CTRL key while clicking an open cell has a slightly more complicated set of rules. If the number of signs surrounding the cell matches the number of mines in its neighborhood, and if each labeled cell is actually a mine, then all adjacent cells that are closed and unlabeled will automatically open. However, if one of the markers is placed on the wrong cell, the game will end in failure.
- The game is won if the player opens all the cells that do not have latent mines.
The data structure
Cell
// JavaScript code that represents a cell:
function Cell( row, column, opened, flagged, mined, neighborMineCount )
{
return {
id: row + "" + column,
row: row,
column: column,
opened: opened,
flagged: flagged,
mined: mined,
neighborMineCount: neighborMineCount
}
}
Copy the code
Each cell is an object containing the following properties:
- id: a string containing rows and columns. Being a unique identifier makes it easier to find cells quickly when needed. If you look carefully, you’ll notice that I used some of the
id
Related shortcuts. I can use these shortcuts because minesweeper has a smaller board, but the code doesn’t scale to a larger board. If you find it, please point it out in the comments! - Row: Integer representing the horizontal position of the cell in the game board.
- Column: Integer representing the vertical position of the cell in the game board.
- Opened: This is a Boolean value that indicates whether a cell is open.
- Flagged: Another Boolean value indicating whether a cell is flagged.
- Mined: Is also a Boolean value that indicates whether or not mines are placed on a cell.
- NeighborMineCount: An integer that represents the number of adjacent cells that contain mines.
Board
// JavaScript code to represent the game board:
function Board( boardSize, mineCount )
{
var board = {};
for( var row = 0; row < boardSize; row++ )
{
for( var column = 0; column < boardSize; column++ )
{
board[row + "" + column] = Cell( row, column, false.false.false.0 );
}
}
board = randomlyAssignMines( board, mineCount );
board = calculateNeighborMineCounts( board, boardSize );
return board;
}
Copy the code
Our game board is a collection of cells. We can represent our game board in many different ways. I chose to represent it as a key-value pair object. As we saw earlier, each cell has an ID to use as a key. The game board is a mapping between these unique keys and their corresponding cells.
After the game board is created, we need to do two more tasks: randomly place mines and count the number of nearby mines. We’ll discuss these tasks in detail in the next section.
algorithm
Random mine placement
// JavaScript code that randomly places mines.
var randomlyAssignMines = function( board, mineCount )
{
var mineCooridinates = [];
for( var i = 0; i < mineCount; i++ )
{
var randomRowCoordinate = getRandomInteger( 0, boardSize );
var randomColumnCoordinate = getRandomInteger( 0, boardSize );
var cell = randomRowCoordinate + "" + randomColumnCoordinate;
while( mineCooridinates.includes( cell ) )
{
randomRowCoordinate = getRandomInteger( 0, boardSize );
randomColumnCoordinate = getRandomInteger( 0, boardSize );
cell = randomRowCoordinate + "" + randomColumnCoordinate;
}
mineCooridinates.push( cell );
board[cell].mined = true;
}
return board;
}
Copy the code
Before the minesweeper game starts, the first thing we need to do is randomly place mines into cells. To do this, I create a function that takes the game board object (board) and the required mineCount (mineCount) as parameters.
For each mine we place, we generate random rows and columns. In addition, the same combination of rows and columns should not be repeated. Otherwise, we will have fewer mines than we would like. If duplication occurs, it must be regenerated randomly.
When generating the coordinates of each random cell, we set the mined attribute to true for the corresponding cell.
I created a helper function to generate random numbers within our expected range. As follows:
// The auxiliary function for generating random numbers:
var getRandomInteger = function( min, max )
{
return Math.floor( Math.random() * ( max - min ) ) + min;
}
Copy the code
Count the number of adjacent mines
// Count the number of adjacent mines
var calculateNeighborMineCounts = function( board, boardSize )
{
var cell;
var neighborMineCount = 0;
for( var row = 0; row < boardSize; row++ )
{
for( var column = 0; column < boardSize; column++ )
{
var id = row + "" + column;
cell = board[id];
if( !cell.mined )
{
var neighbors = getNeighbors( id );
neighborMineCount = 0;
for( var i = 0; i < neighbors.length; i++ ) { neighborMineCount += isMined( board, neighbors[i] ); } cell.neighborMineCount = neighborMineCount; }}}return board;
}
Copy the code
Now let’s look at how to count mines in adjacent cells.
You’ll notice that we loop through every row and every column on the game board, which is a very common way. This way we can perform the same processing on each cell.
We first check if each cell is mined. If yes, there is no need to check the number of adjacent mines. After all, if the player clicks on it, he/she will lose the game
If the cell is not mined, then we need to see how many mines are around it. The first thing we need to do is call the getNeighbors helper function, which returns a list of ids for the adjacent cells. We then loop through the list, adding up the number of mines and updating the neighborMineCount attribute of the cell.
Gets adjacent cells
Let’s take a closer look at the getNeighbors function, because it will be called multiple times throughout the code. As I mentioned earlier, some of my design approaches come from not having to scale to a larger game board. The same is true here:
// The JavaScript code to get all the adjacent ids of the minesweeper cell:
var getNeighbors = function( id )
{
var row = parseInt(id[0]);
var column = parseInt(id[1]);
var neighbors = [];
neighbors.push( (row - 1) + "" + (column - 1)); neighbors.push( (row -1) + "" + column );
neighbors.push( (row - 1) + "" + (column + 1)); neighbors.push( row +"" + (column - 1)); neighbors.push( row +"" + (column + 1)); neighbors.push( (row +1) + "" + (column - 1)); neighbors.push( (row +1) + "" + column );
neighbors.push( (row + 1) + "" + (column + 1));for( var i = 0; i < neighbors.length; i++)
{
if ( neighbors[i].length > 2 )
{
neighbors.splice(i, 1); i--; }}return neighbors
}
Copy the code
This function takes a cell ID as an argument. And then we immediately split it into two pieces so that we have the row and column values. We use the built-in function parseInt to convert a string to an integer. Now we can do the math on them.
Next, we calculate the ID of each adjacent cell using rows and columns and add them to the list. Before processing the situation, the list should contain eight ids.
A cell and its adjacent cells.
While this is fine for the general case, there are some special cases we need to consider. This is the cell at the edge of the game board. These cells will have fewer than eight adjacent cells.
To solve this problem, we loop over the ids of adjacent cells and remove ids greater than 2 in length. All invalid adjacent cell rows or columns could be -1 or 10, so this problem is neatly solved.
Whenever an ID is removed from the list, we also have to reduce the index variable to keep it in sync.
Judge mines
Well, we have one last function to discuss in this section: isMined.
// A JavaScript function to check if a cell is a mine:
var isMined = function( board, id )
{
var cell = board[id];
var mined = 0;
if( typeofcell ! = ='undefined' )
{
mined = cell.mined ? 1 : 0;
}
return mined;
}
Copy the code
The isMined function is very simple. It simply checks if the cell is a mine. If so, return 1; Otherwise, 0 is returned. This feature allows us to accumulate the return value of a function as it is called repeatedly through the loop.
This completes the algorithm for setting up the minesweeper game board. Let’s get into the real game!
Flip cell
// Execute JavaScript code when the cell is opened:
var handleClick = function( id )
{
if( !gameOver )
{
if( ctrlIsPressed )
{
handleCtrlClick( id );
}
else
{
var cell = board[id];
var $cell = $( The '#' + id );
if( !cell.opened )
{
if( !cell.flagged )
{
if( cell.mined )
{
loss();
$cell.html( MINE ).css( 'color'.'red');
}
else
{
cell.opened = true;
if( cell.neighborMineCount > 0 )
{
var color = getNumberColor( cell.neighborMineCount );
$cell.html( cell.neighborMineCount ).css( 'color', color );
}
else
{
$cell.html( "" )
.css( 'background-image'.'radial-gradient(#e6e6e6,#c9c7c7)');
var neighbors = getNeighbors( id );
for( var i = 0; i < neighbors.length; i++ )
{
var neighbor = neighbors[i];
if( typeofboard[neighbor] ! = ='undefined'&&! board[neighbor].flagged && ! board[neighbor].opened ) { handleClick( neighbor ); } } } } } } } } }Copy the code
Well, let’s get right into the action of the stimulus. We execute this function every time the player clicks on a cell. It does a lot of work, and it uses recursion. If you are not familiar with this concept, please refer to the following definitions:
Recursion: See Recursion.
Haha, what a joke in computer science. It’s always fun to do this in a bar or cafe. You really should try it on that cute girl you have a crush on.
Anyway, a recursive function is a function that calls itself. Sounds like a stack overflow could happen, right? That’s why you need a basic condition that you don’t make any subsequent recursive calls. Our function will eventually stop calling itself because no more cells need to be opened.
Recursion is rarely the right choice in real projects, but it can be a useful tool. We could have written this code without recursion, but I thought you might want to see an example of it in action.
Click the cell
The handleClick function accepts the cell ID as an argument. We need to deal with the case where the player presses the CTRL key when clicking a cell, but we’ll discuss this later.
Assuming the game isn’t over yet and we’re dealing with a basic left-click event, we need to do some checking. If the player has already opened or marked the cell, we should ignore the click event. It can be frustrating if the player accidentally clicks on a marked cell and the game ends.
If these two conditions are not met, then we will continue. If there are mines in the cell, we need to deal with the logic of the game failing and show the exploded mines red. Otherwise, we will set the cell to open.
If there are mines around the open cell, we will show the player the number of mines in the vicinity with the appropriate font color. If there are no mines around the cell, then it’s time to use recursion. After setting the background color of the cell to a slightly darker gray, we use handleClick for each adjacent cell that is not opened and not marked.
Auxiliary function
Let’s look at the helper functions used in the handleClick function. We’ve already talked about getNeighbors, so let’s start with the Loss function.
// JavaScript code called when the player loses the game:
var loss = function()
{
gameOver = true;
$('#messageBox').text('Game Over! ')
.css({'color':'white'.'background-color': 'red'});
var cells = Object.keys(board);
for( var i = 0; i < cells.length; i++ )
{
if( board[cells[i]].mined && ! board[cells[i]].flagged ) { $(The '#' + board[cells[i]].id ).html( MINE )
.css('color'.'black');
}
}
clearInterval(timeout);
}
Copy the code
When the game fails, we set the value of the global gameOver variable and display a message letting the player know that the game is over. We also loop through each cell and show where the mine occurred. Then we stop the timer.
Second, we have the getNumberColor function. This function is responsible for giving the color of the number of mines displayed in adjacent cells.
// Pass in a number and return a color:
var getNumberColor = function( number )
{
var color = 'black';
if( number === 1 )
{
color = 'blue';
}
else if( number === 2 )
{
color = 'green';
}
else if( number === 3 )
{
color = 'red';
}
else if( number === 4 )
{
color = 'orange';
}
return color;
}
Copy the code
I tried to match the colors, like the classic Windows version of Minesweeper. Maybe I should have used the switch statement, but I’m not thinking about the game being extended anymore, it’s not a big deal. Let’s move on to the logical code that marks the cells.
Labeled cell
// JavaScript code for placing tags on cells:
var handleRightClick = function( id )
{
if( !gameOver )
{
var cell = board[id];
var $cell = $( The '#' + id );
if( !cell.opened )
{
if( !cell.flagged && minesRemaining > 0 )
{
cell.flagged = true;
$cell.html( FLAG ).css( 'color'.'red');
minesRemaining--;
}
else if( cell.flagged )
{
cell.flagged = false;
$cell.html( "" ).css( 'color'.'black');
minesRemaining++;
}
$( '#mines-remaining').text( minesRemaining ); }}}Copy the code
Right-clicking on a cell places a marker on it. Flagged cells will be flagged by red flags if players right click on an unflagged cell and there are currently remaining mines to be flagged, updated to true and flagged to reduce the number of remaining mines. If the cell already has a flag, do the opposite. Finally, we update the number of remaining mines shown.
Flip through all adjacent cells
// Handles JavaScript code for the CTRL + left key
var handleCtrlClick = function( id )
{
var cell = board[id];
var $cell = $( The '#' + id );
if( cell.opened && cell.neighborMineCount > 0 )
{
var neighbors = getNeighbors( id );
var flagCount = 0;
var flaggedCells = [];
var neighbor;
for( var i = 0; i < neighbors.length; i++ )
{
neighbor = board[neighbors[i]];
if( neighbor.flagged )
{
flaggedCells.push( neighbor );
}
flagCount += neighbor.flagged;
}
var lost = false;
if( flagCount === cell.neighborMineCount )
{
for( i = 0; i < flaggedCells.length; i++ )
{
if( flaggedCells[i].flagged && ! flaggedCells[i].mined ) { loss(); lost =true;
break; }}if( !lost )
{
for( var i = 0; i < neighbors.length; i++ )
{
neighbor = board[neighbors[i]];
if(! neighbor.flagged && ! neighbor.opened ) { ctrlIsPressed =false;
handleClick( neighbor.id );
}
}
}
}
}
}
Copy the code
We’ve already covered opening and marking cells, so let’s look at the last action a player can take: opening an adjacent cell in an open state. The handleCtrlClick function is used to handle this logic. You can do this by holding Down CTRL and left-clicking on an open cell that contains an adjacent mine.
If so, the first thing we need to do is create a list of adjacent marked cells. If the number of adjacent labeled cells matches the actual number of surrounding mines, we proceed. Otherwise, we do nothing and just exit the function.
If you continue, the next thing you need to do is check if the marked cells contain mines. If it is, we know that the player has mispredicted the location of the mines and will have to flip through all unmarked adjacent cells, causing the game to fail. We need to set the local variable lost and call the Loss function. The Loss function has been discussed previously.
If the game still does not fail, we will need to open all unmarked adjacent cells. We just need to loop through them and call the handleClick function on each one. However, we must first set the ctrlIsPressed variable to false to prevent incorrect execution of the handleCtrlClick function.
Start the game
We’ve almost finished analyzing all the JavaScript logic needed to write minesweeper! All that remains to be discussed are the initialization steps required to start a new game.
// The JavaScript code used to initialize minesweeper
var FLAG = "The & # 9873;";
var MINE = "The & # 9881;";
var boardSize = 10;
var mines = 10;
var timer = 0;
var timeout;
var minesRemaining;
$(document).keydown(function(event){
if(event.ctrlKey)
ctrlIsPressed = true;
});
$(document).keyup(function(){
ctrlIsPressed = false;
});
var ctrlIsPressed = false;
var board = newGame( boardSize, mines );
$('#new-game-button').click( function(){
board = newGame( boardSize, mines );
})
Copy the code
The first thing we need to do is initialize some variables. We need to define constants to store the flags and mine ICONS in the HTML code. We also need constants to store the size of the game board, the total number of mines, the timer, and the number of remaining mines.
Also, if the player pressed the CTRL key, we need a variable to store whether or not the CTRL key was pressed. We used jQuery to add an event handler to the document to set the value of the ctrlIsPressed variable.
Finally, we call the newGame function and bind it to the New Game button.
Auxiliary function
// JavaScript code to start a new minesweeper game
var newGame = function( boardSize, mines )
{$('#time').text("0");
$('#messageBox').text('Make a Move! ')
.css({'color': 'rgb(255, 255, 153)'.'background-color': 'rgb(102, 178, 255)'});
minesRemaining = mines;
$( '#mines-remaining').text( minesRemaining );
gameOver = false;
initializeCells( boardSize );
board = Board( boardSize, mines );
timer = 0;
clearInterval(timeout);
timeout = setInterval(function () {
// This will be executed after 1,000 milliseconds
timer++;
if( timer >= 999 )
{
timer = 999;
}
$('#time').text(timer);
}, 1000);
return board;
}
Copy the code
The newGame function is responsible for resetting variables to make our game ready to play. This includes resetting the message displayed to the player, calling initializeCells, and creating a new random game board. It also includes a reset timer that is updated every second.
Let’s summarize by looking at initializeCells.
// JavaScript code for attaching a click handler to a cell and checking for victory conditions
var initializeCells = function( boardSize )
{
var row = 0;
var column = 0;
$( ".cell" ).each( function(){$(this).attr( "id", row + "" + column ).css('color'.'black').text("");
$(The '#' + row + "" + column ).css('background-image'.'radial-gradient(#fff,#e6e6e6)');
column++;
if( column >= boardSize )
{
column = 0;
row++;
}
$(this).off().click(function(e)
{
handleClick( $(this).attr("id"));var isVictory = true;
var cells = Object.keys(board);
for( var i = 0; i < cells.length; i++ )
{
if( !board[cells[i]].mined )
{
if( !board[cells[i]].opened )
{
isVictory = false;
break; }}}if( isVictory )
{
gameOver = true;
$('#messageBox').text('You Win! ').css({'color': 'white'.'background-color': 'green'}); clearInterval( timeout ); }}); $(this).contextmenu(function(e)
{
handleRightClick( $(this).attr("id"));return false; }); })}Copy the code
The main purpose of this function is to add additional attributes to the cell DOM object. Each cell DOM needs to have a corresponding ID added so that it can be easily accessed from the game logic. Each cell also needs a suitable background image.
We also need to add a click handler for each cell DOM to be able to listen for left – and right-click events.
Call the handleClick function to handle the left-click event, passing in the corresponding ID. Then check that every mine-free cell is opened. If this is true, then the game wins and we can congratulate him/her appropriately.
To handle the right click event, call handleRightClick, again passing in the corresponding ID, and return false. This prevents the default behavior of right-clicking a Web page to display a context menu. You might not want to do this for a typical CRUD application, but for minesweeper games, it’s appropriate.
conclusion
Congratulations, you’ve learned how to write minesweeper in JavaScript! It seems like a lot of code, but hopefully it makes sense for us to break it down into different modules like this. We can definitely make more improvements to the reusability, extensibility, and readability of this program. We also didn’t go into HTML or CSS code in detail. If you have any questions or ways to improve the code, I’d love to hear from you in the comments!
If this article makes you want to learn more about how to write better programs in JavaScript, I recommend a JavaScript book: the essence of JavaScript By Douglas Crockford. He popularized JSON as a format for data exchange and contributed greatly to the development of the Web.
The JavaScript language has improved greatly over the years, but it still has some strange features due to its history. This book will help you better understand the language’s design problems (such as global namespaces). When I first learned the language, I found it very helpful.
If you decide to own it and purchase it via the link above I would be very grateful to you. I will earn some commissions through Amazon’s membership program at no extra cost to you. It will help me keep the site up and running without resorting to annoying ads. I’d rather recommend anything I think will be helpful.
All right, that’s the end of the commercial. I hope you have a pleasant reading experience. Let me know what other similar simple games you’d like to see and don’t forget to leave your email so you don’t miss out on writing an article. You’ll also receive my free push on how to write functions better.
I wish good!
Update (2019/7/13) : This post is more popular than I thought it would be, great! I received a lot of feedback from readers about things that could be improved. I work on a daily basis to maintain a codebase that until now has been stuck in Internet Explorer weird mode. Many of my coding habits at work were transferred to my minesweeper games, resulting in some code that didn’t take advantage of the cutting edge of JavaScript technology. After that, I want to refactor the code in another article. I plan to remove jQuery completely and use ES6 syntax instead of ES5 where appropriate. But you don’t have to wait for me! See if you can do it yourself! Let me know how it goes in the comments.
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.