Creating a Minesweeper game in Javascript tutorial
Posted on 21. Dec, 2009 by tak3r in Web Tutorials 1,214 views
How to create a fully functional minesweeper game in javascript
Hello world!
This tutorial is about creating one of the most popular windows game ever: MINESWEEPER ^.^
Before we start we have to make a plan regarding what are the components of our game. So, what are the components, the key functions of minesweeper?
This is the end producs that we are going to create: http://tak3r.com/tutorials/download/minesweeper.html
Give it a 5 minute thought and see if you come up with the folowing list:
- Grid generator function
- Mine placing function
- Field discover function
- Flag placing function
- Multiple flield flip function. ( I’ll explain later on)
- Win/loose events
- And a reset button for loosers
For the sake of logic we will do them in that order.
We will do the same file both the javascript and the HTML+css. Well… we can finish the html right now. All the html we will ever need to write for this game is:
<div id="m"> </div>
This is just the place where we will transform into a minesweeper field. The rest is ALL javascript+css. Lets finish with the css also, so it won’t bother us later. I have created the game ahead and used just some minor CSS style to go around. If you want you can enhance it with background images of fields, flags and mines, but tipically I used just 3 colors: lightblue for a discovered field, red for a mine field and orange for a flag field. So, here’s the code:
<style type="text/css">
#m {
display:inline-block;
background:#55AAFF;
border:3px outset #aaaaaa;
padding:4px;
}
#m #status{ border:2px inset #eeeeee; width:100%; margin:1px 1px 3px 1px; color:#fff; }
#m #status input { text-align:center; }
#m #mineField{ border:2px inset #eeeeee; padding:1px; }
#m span{
display:inline-block;
background:#dddddd;
width:15px;
height:15px;
margin:1px 1px 0 0;
font-family:georgia;
font-size:8pt;
text-align:center;
cursor:pointer;
}
#m #minesLeft{ font-weight:bold; }
#m .tile{ }
#m .mine{ background-color:red; }
#m .clue{ background-color:lightblue; font-weight:bold; }
#m .flag{ background-color:orange; }
</style>
In case you don’t know, the CSS is inserted in the <head> part of the page, but it works fine in the <body> also. As for the css, it is pretty straight forward and I don’t think It must be explained any further. Now starts the fun part: the javascript. We will start off by adding the attribute: onload=”setupMinesweeper(‘m’);” to the <body> tag. What this says is that when the document has finished loading run the setupMinesweeper(‘m’) function. This is the function we are going to make now and the argument is the id tag of the field we want to place it in as string, in our case ‘m’. Having done that move to your <script> tag and ’till the end of the episode we will work only there. We start off by making the setupMinesweeper function that initially creates the fields:
var mWidth = 30;
var mHeight = 24;
var mNumber = 80;
var mStatus = 1; // 0-lose 1-playing 2-win
var mFlags = mNumber;
var mFields = 0;
var mArr = [];
var mClues = [];
var mPinned = [];
var mColors = ['#000000', '#0000FF', '#008200', '#FF0000', '#000084', '#840000', '#008284', '#840084', '#FF7F00', '#FF00FF', '#FFFFFF'];
function setupMinesweeper(id){
mStatus = 1;
mFlags = mNumber;
mFields = 0;
mArr.length = 0;
mClues.length = 0;
var h = '<div id="mineField">';
for(i=1;i<=mHeight;++i){
for(j=1;j<=mWidth;++j){
h += '<span id="m'+i+'x'+j+'" class="tile" onclick="setFlag(this);" ondblclick="revealField(this)"> </span>';
}
h += '<br />';
}
h += '</div>';
h = '<table id="status"><td align="left"><input type="button" value="Reset" onclick="setupMinesweeper(\'m\');" /></td>'+
'<td style="text-align:right">Flags left: <label id="minesLeft">'+mFlags+'</label> </td></tr></table>' + h;
document.getElementById(id).innerHTML = h;
}
//
I have also added some global variables that we will need later on:
mWidth – the number of columns
mHeight – the number of rows
mNumber – the number of mines
mStatus – the current game status. Initially it is set to 1 for playing
mFlags – the number of flags which we add the number of mines. Initially it is equal but it decreses as you place flags…DUUUH
mFields - the number of discovered fields.
mArr - in this array we store the position of the mines
mClues – and here we store the clues and their position
mPinned – an array where we will store the position of the flags placed by the player
mColors – an array of colors. We will attribute each of these colors to a specific number (blue for 1, green for 2, etc.)
I have reassigned these variables their default values again in the setupMinesweeper() function because we will need to reset these when we want to restart the game so it is best to do it from now.
In case you did not know, in javascript if you attribute a variable the value [] it is the same thing as giving it the value array(). For example: var a = array() is the same
thing as var a = []. I also left a space between mNumber and mStatus to split the variables any user can modify (number of mines, width and height of a field)
o the ones that must be global and that are modified only by the script itself
Now take a look at the function up there. This function is equivalent to the function of filling a matrix with values. So what our function does is for every row (mHeight)
it creates a mWidth number of fields resulting in a mHeight x mWidth number of fields. In the same time it turns the mArr[] and mClues arryas into matrices.
For each field we attribute an unique id that will allow us to find the position of that field with the help of its id. Basically the id of a field is composed out of:
the letter m + its position on the Y axis + the letter x + its position on the X axis.
I have also added to more attributes that are quite self explanatory. When a user CLICKS once on a field it will attempt to place a flag on it, when he DOUBLECLICKS the field will
reveal what is “underneath” it: a mine or a hint.
There is also a field added under the field matrix where we tell a user how many flags he has left to place and then we insert all this html code into the container we gave as
an argument for our function, in our case ‘m’.
If you test your code now you should see the field matrix.
Moving on, we need to place the mines on the field and their corresponding hints. I have done this also in the setupMinesweeper() function for ease of access, after all,
adding the mines is part of the setup process. So, here is the enhanced function:
function setupMinesweeper(id){
mStatus = 1;
mFlags = mNumber;
mFields = 0;
mArr.length = 0;
mClues.length = 0;
var h = '<div id="mineField">';
for(i=1;i<=mHeight;++i){
for(j=1;j<=mWidth;++j){
h += '<span id="m'+i+'x'+j+'" class="tile" onclick="setFlag(this);" ondblclick="revealField(this)"> </span>';
}
h += '<br />';
}
h += '</div>';
h = '<table id="status"><td align="left"><input type="button" value="Reset" onclick="setupMinesweeper(\'m\');" /></td>'+
'<td style="text-align:right">Flags left: <label id="minesLeft">'+mFlags+'</label> </td></tr></table>' + h;
document.getElementById(id).innerHTML = h;
for(i=0;i<=mHeight+1;++i){
mArr[i] = [];
mClues[i] = [];
mPinned[i] = [];
for(j=0;j<=mWidth+1;++j){
mArr[i][j] = 0;
mClues[i][j] = 0;
mPinned[i][j] = 0;
}
}
for(i=1;i<=mNumber;++i){
var idR = Math.floor(Math.random() * mHeight)+1;
var idC = Math.floor(Math.random() * mWidth)+1;
while(mArr[idR][idC] == 1 || (idR==1 && idC==1) ){
idR = Math.floor(Math.random() * mHeight)+1;
idC = Math.floor(Math.random() * mWidth)+1;
}
mArr[idR][idC] = 1;
}
for(i=1;i<=mHeight;++i){
for(j=1;j<=mWidth;++j){
var no = 0;
if(mArr[i][j] == 0) no = mArr[i-1][j-1]+mArr[i-1][j]+mArr[i-1][j+1]+mArr[i][j-1]+mArr[i][j+1]+mArr[i+1][j-1]+mArr[i+1][j]+mArr[i+1][j+1];
mClues[i][j] = no;
}
}
}
//
Ok, as you might have noticed i have added 2 more for-s. The first one adds mines on the field… in fact it adds mines on the mArr[][] matrix but it is the same thing. The process
is fairly simple. It selects a random number between 1 and mWidth and another random number between 1 and mHeight. These are the idR and idC (stands for: “the ID of the Row” and
“the ID of the Column”). After that it goes innto a while that states the following: “while at the coordinates of the random set of numbers you have chosen randomly is a mine OR
you have chosen to place a mine on the top left corner field, choose another set of random numbers and try again untill”. So basically it searches for a field that does not
have a mine and that is different from the top left corner field (that field is never a mine because the guys from microsoft considered that you MUST have 1 field from where
to start the game) and gives it the value 1 which means that it is a mine.
The last for adds the hints for the mines. It goes and checks every field of the matrix to see if it is empty (not a mine). If it is then it gives it the value of the number of
mines around it.
Moving on with our coding:
function setFlag(x){
var tmp = x.id.slice(1).split('x');
var i = Number(tmp[0]);
var j = Number(tmp[1]);
if(mStatus==1){
if(!hasClass(x,'clue') && !hasClass(x,'mine')) {
if(!hasClass(x,'flag') && mFlags>0) { x.className = 'flag'; --mFlags; mPinned[i][j] = 1; }
else if(hasClass(x,'flag')) { x.className = 'tile'; ++mFlags; mPinned[i][j] = 0; }
document.getElementById('minesLeft').innerHTML = mFlags;
if(mFlags==0) checkWin();
} else return;
}
}
function hasClass(obj, c){
var tmp = obj.className.split(' ');
for (i in tmp) if(tmp[i] == c) return true;
return false;
}
function checkWin(){
if(mFlags==0){
var w = true;
for(a=1;a<=mHeight;++a) for(b=1;b<=mWidth;++b) if(mArr[a][b]!=mPinned[a][b]) win = false;
if(w) win(); else return;
} else return;
}
//
Here we have the flag function, a checkWin() function and a small helpful boolen function that searches to see if an item has a specific class. Why we need this? Because at the next step when we start revealing fields we will mark each field by giving it a class specific to the tipe of field it is. So, there are a couple of if-s there:
-the first one checks to see if we are still playing (if we lost of won it is just stupid to be able to place flags)
-the second one checks to see if the field is NOT discovered.
-the third if checks to see whether the field is a flag field or not and if we have any more flags to place. if it is not a flag and we still have flags then it adds it the “flag” class and decrements the flags left variable, otherwise if it is a flag it unflags it and increments the mFlags var.
-the forth and final one check to see if we still have flags to place. if we don’t (mFlags==0) then it summons the checkWin() function. What this does it compare the mine matrix with the flag matrix. if they overlap perfectly then we won.
And in the end we update the “minesLeft” field we inserted when we called the setupMinesweeper() function
And now come the 3 big functions:
function revealField(obj){
if(mStatus==1){
var tmp = obj.id.slice(1).split('x');
var i = Number(tmp[0]);
var j = Number(tmp[1]);
if(mArr[i][j] == 1) lose(); else {
for(a=i-1;a<=i+1;a++) for(b=j-1;b<=j+1;b++) if(mClues[a][b]==0) rollback(a,b,1);
}
showField(i,j);
}
}
function rollback(i,j) {
if (i > 0 && i <= mHeight && j >0 && j <= mWidth)
if(mClues[i][j] == 0 && mArr[i][j]==0){
var c = 0;
for(a=i-1;a<=i+1;a++) for(b=j-1;b<=j+1;b++) if(a!=i&&b!=j) c+=mArr[a][b];
mClues[i][j] = 10;
showField(i,j);
if (c == 0) {
rollback(i-1,j-1);
rollback(i-1,j);
rollback(i-1,j+1);
rollback(i,j-1);
rollback(i,j+1);
rollback(i+1,j-1);
rollback(i+1,j);
rollback(i+1,j+1);
}
} else if(mClues[i][j]!=10 && mArr[i][j]==0) showField(i,j);
}
function showField(i,j){
var obj = document.getElementById('m'+i+'x'+j);
if(mArr[i][j] == 1){
obj.className ='mine';
} else {
if(hasClass(obj,'clue')) return;
if(hasClass(obj,'flag')) {++mFlags; mPinned[i][j] = 0; document.getElementById('minesLeft').innerHTML = mFlags;}
++mFields;
obj.className ='clue';
obj.innerHTML = mClues[i][j]<9?'<font style="color:'+mColors[mClues[i][j]]+'">'+mClues[i][j]+'</font>':' ';
checkWin();
}
}
// Although they might seem scarry, there is not that much to explain here.
The showField() function basically adds the proper attribute and value to the field being checked. At the end, if the total number of visible fields plus the total number of mines is equal to the total number of fields it calls a win() function.
It also adds the specific color to each specific number. I just took those colors from my minesweeper, they are pretty general so nothing unusual there.
The rollback() function is a recursive function. A recursive function is that which calls itself. Its arguments are the X and Y coordinates of the field to be checked.
So, first it checks if the arguments entered are in the game matrix. If it is, and if is not a mine and it is not discovered yet it counts the mines around it. If the number
of the mind around the field is 0 it shows it and checks all calls the rollback function again on each of the fields surrounding it(8 calls).
And the revealField() function. This is the one who tells the other functions what to do. It initially check to see if we are still playing. If we are it gets the coordinates
of the field that was doubleclicked. It gets its matrix coordinates by removing the first caracter of its ID which is always ‘m’ and spliting it by the value ‘x’. What is on the
left is its position on the Y axis and what is on the right is its position on the X axis.
It then check to see if it is a minne. If it is, we have lost. If not, it checks the sum of all the mines around it. If it is 0 it calls the rollback() function I told you about
earlier, if not then it just reveals the field.
Finally we end this tutorial up by writing our final lines of code, the win() and lose() functions:
function win(){
alert('you freakin\' won!');
mStatus = 2;
}
function lose(){
for(a=1;a<=mHeight;++a) for(b=1;b<=mWidth;++b) if(mArr[a][b]==1) document.getElementById('m'+a+'x'+b).className='mine';
mStatus = 0;
}
Nothing much to say here… If we won we summon an alert box with a message, if not we show where all the mines were.
all together now:
<style type="text/css">
#m {
display:inline-block;
background:#55AAFF;
border:3px outset #aaaaaa;
padding:4px;
}
#m #status{ border:2px inset #eeeeee; width:100%; margin:1px 1px 3px 1px; color:#fff; }
#m #status input { text-align:center; }
#m #mineField{ border:2px inset #eeeeee; padding:1px; }
#m span{
display:inline-block;
background:#dddddd;
width:15px;
height:15px;
margin:1px 1px 0 0;
font-family:georgia;
font-size:8pt;
text-align:center;
cursor:pointer;
}
#m #minesLeft{ font-weight:bold; }
#m .tile{ }
#m .mine{ background-color:red; }
#m .clue{ background-color:lightblue; font-weight:bold; }
#m .flag{ background-color:orange; }
</style>
<script type="text/javascript"`>
var mWidth = 30;
var mHeight = 24;
var mNumber = 80;
var mStatus = 1; // 0-lose 1-playing 2-win
var mFlags = mNumber;
var mFields = 0;
var mArr = [];
var mClues = [];
var mPinned = [];
var mColors = ['#000000', '#0000FF', '#008200', '#FF0000', '#000084', '#840000', '#008284', '#840084', '#FF7F00', '#FF00FF', '#FFFFFF'];
function setupMinesweeper(id){
mStatus = 1;
mFlags = mNumber;
mFields = 0;
mArr.length = 0;
mClues.length = 0;
var h = '<div id="mineField">';
for(i=1;i<=mHeight;++i){
for(j=1;j<=mWidth;++j){
h += '<span id="m'+i+'x'+j+'" class="tile" onclick="setFlag(this);" ondblclick="revealField(this)"> </span>';
}
h += '<br />';
}
h += '</div>';
h = '<table id="status"><td align="left"><input type="button" value="Reset" onclick="setupMinesweeper(\'m\');" /></td>'+
'<td style="text-align:right">Flags left: <label id="minesLeft">'+mFlags+'</label> </td></tr></table>' + h;
document.getElementById(id).innerHTML = h;
for(i=0;i<=mHeight+1;++i){
mArr[i] = [];
mClues[i] = [];
mPinned[i] = [];
for(j=0;j<=mWidth+1;++j){
mArr[i][j] = 0;
mClues[i][j] = 0;
mPinned[i][j] = 0;
}
}
for(i=1;i<=mNumber;++i){
var idR = Math.floor(Math.random() * mHeight)+1;
var idC = Math.floor(Math.random() * mWidth)+1;
while(mArr[idR][idC] == 1 || (idR==1 && idC==1) ){
idR = Math.floor(Math.random() * mHeight)+1;
idC = Math.floor(Math.random() * mWidth)+1;
}
mArr[idR][idC] = 1;
}
for(i=1;i<=mHeight;++i){
for(j=1;j<=mWidth;++j){
var no = 0;
if(mArr[i][j] == 0) no = mArr[i-1][j-1]+mArr[i-1][j]+mArr[i-1][j+1]+mArr[i][j-1]+mArr[i][j+1]+mArr[i+1][j-1]+mArr[i+1][j]+mArr[i+1][j+1];
mClues[i][j] = no;
}
}
}
function revealField(obj){
if(mStatus==1){
var tmp = obj.id.slice(1).split('x');
var i = Number(tmp[0]);
var j = Number(tmp[1]);
if(mArr[i][j] == 1) lose(); else {
for(a=i-1;a<=i+1;a++) for(b=j-1;b<=j+1;b++) if(mClues[a][b]==0) rollback(a,b,1);
}
showField(i,j);
}
}
function rollback(i,j) {
if (i > 0 && i <= mHeight && j >0 && j <= mWidth)
if(mClues[i][j] == 0 && mArr[i][j]==0){
var c = 0;
for(a=i-1;a<=i+1;a++) for(b=j-1;b<=j+1;b++) if(a!=i&&b!=j) c+=mArr[a][b];
mClues[i][j] = 10;
showField(i,j);
if (c == 0) {
rollback(i-1,j-1);
rollback(i-1,j);
rollback(i-1,j+1);
rollback(i,j-1);
rollback(i,j+1);
rollback(i+1,j-1);
rollback(i+1,j);
rollback(i+1,j+1);
}
} else if(mClues[i][j]!=10 && mArr[i][j]==0) showField(i,j);
}
function showField(i,j){
var obj = document.getElementById('m'+i+'x'+j);
if(mArr[i][j] == 1){
obj.className ='mine';
} else {
if(hasClass(obj,'clue')) return;
if(hasClass(obj,'flag')) {++mFlags; mPinned[i][j] = 0; document.getElementById('minesLeft').innerHTML = mFlags;}
++mFields;
obj.className ='clue';
obj.innerHTML = mClues[i][j]<9?'<font style="color:'+mColors[mClues[i][j]]+'">'+mClues[i][j]+'</font>':' ';
checkWin();
}
}
function setFlag(x){
var tmp = x.id.slice(1).split('x');
var i = Number(tmp[0]);
var j = Number(tmp[1]);
if(mStatus==1){
if(!hasClass(x,'clue') && !hasClass(x,'mine')) {
if(!hasClass(x,'flag') && mFlags>0) { x.className = 'flag'; --mFlags; mPinned[i][j] = 1; }
else if(hasClass(x,'flag')) { x.className = 'tile'; ++mFlags; mPinned[i][j] = 0; }
document.getElementById('minesLeft').innerHTML = mFlags;
if(mFlags==0) checkWin();
} else return;
}
}
function hasClass(obj, c){
var tmp = obj.className.split(' ');
for (i in tmp) if(tmp[i] == c) return true;
return false;
}
function checkWin(){
if(mFlags==0){
var w = true;
for(a=1;a<=mHeight;++a) for(b=1;b<=mWidth;++b) if(mArr[a][b]!=mPinned[a][b]) win = false;
if(w) win(); else return;
} else return;
}
function win(){
alert('you freakin\' won!');
mStatus = 2;
}
function lose(){
for(a=1;a<=mHeight;++a) for(b=1;b<=mWidth;++b) if(mArr[a][b]==1) document.getElementById('m'+a+'x'+b).className='mine';
mStatus = 0;
}
</script>
Not that hard, is it?
