jQuery Powered Mine Sweeper Game

Posted October 27, 2009 at 10:17 AM

Tags: Javascript / DHTML

While I was on the road going from CFinNC to BFusion / BFLEX, I tinkered around with some jQuery code. I wanted to try and make something fun but not something that would take too much time (since being on the road is exhausting). And so, I created this jQuery powered Mine Sweeper game. Much like the old computer game, you can select the number of rows, columns, and bombs (using either an explicit number or a percentage). Clicking on the cell reveals the number of surrounding bombs. Clicking, while holding the ALT key, flags the cells as a potential bomb.

 
 
 
 
 
 
 
 
 
 

If you want to try the game for yourself, click on one of the links. Notice that the URL variables control the display:

jQuery Mine Sweeper - Easy

jQuery Mine Sweeper - Medium

jQuery Mine Sweeper - Hard

The markup for this game is extremely simple as the real logic has been factored out into a number of Javascript files:

 Launch code in new window » Download code as text file »

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>jQuery Powered Mine Sweeper</title>
  • <style type="text/css">
  •  
  • body.caution {
  • cursor: help ;
  • }
  •  
  • table.mine-sweeper {}
  •  
  • table.mine-sweeper td {
  • background-color: #F0F0F0 ;
  • border: 1px solid #333333 ;
  • cursor: pointer ;
  • height: 25px ;
  • line-height: 25px ;
  • overflow: hidden ;
  • text-align: center ;
  • width: 25px ;
  • }
  •  
  • table.mine-sweeper td.active {
  • background-color: #D8D8D8 ;
  • }
  •  
  • table.mine-sweeper td.bomb {
  • font-weight: bold ;
  • }
  •  
  • table.mine-sweeper td.bombed {
  • background-color: #FFD0D0 ;
  • color: #CC0000 ;
  • font-weight: bold ;
  • }
  •  
  • table.mine-sweeper td.caution {
  • background-color: #FFD0D0 ;
  • color: #CC0000 ;
  • font-weight: bold ;
  • }
  •  
  • </style>
  • <script type="text/javascript" src="jquery-1.3.2.min.js"></script>
  • <script type="text/javascript" src="jquery.randrange.js"></script>
  • <script type="text/javascript" src="jquery.repeatstring.js"></script>
  • <script type="text/javascript" src="jquery.randomfilter.js"></script>
  • <script type="text/javascript" src="jquery.near.js"></script>
  • <script type="text/javascript" src="jquery.minesweeper.js"></script>
  • <script type="text/javascript">
  •  
  • // When the DOM is ready, build the mine sweeper game.
  • jQuery(function( $ ){
  •  
  • var mineSweerper = new MineSweeper(
  • $( "table.mine-sweeper" ),
  • 17,
  • 12,
  • "99%"
  • );
  •  
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • jQuery Powered Mine Sweeper
  • </h1>
  •  
  • <table cellspacing="2" class="mine-sweeper">
  • <!--- Will be populated dynamically. --->
  • </table>
  •  
  • </body>
  • </html>

The game has to be played in a TABLE node because the game logic requires the structured relationship between table cells. The game controller itself is wrapped up in this Javascript class:

jquery.minesweeper.js

 Launch code in new window » Download code as text file »

  • (function( $ ){
  •  
  • // I am the controller for the mine sweeper game.
  • function MineSweeper( selector, columnCount, rowCount, bombCount ){
  • var self = this;
  • this.table = $( selector );
  • this.columnCount = (columnCount || 30);
  • this.rowCount = (rowCount || 30);
  •  
  • // Check to see if the bomb count contains a percent sign.
  • if (
  • (typeof( bombCount ) == "string") &&
  • (bombCount.indexOf( "%" ) > 0)
  • ){
  •  
  • // The bomb count is a percentage of the total number
  • // of cells.
  • this.bombCount = Math.floor(
  • (this.columnCount * this.rowCount) *
  • (parseInt( bombCount ) / 100)
  • );
  •  
  • } else {
  •  
  • // The bomb count is just a standard number.
  • this.bombCount = (bombCount || 15);
  •  
  • }
  •  
  • // Bind the click handler for the table. This way, we
  • // don't have to attach event handlers to each cell.
  • this.table.click(
  • function( event ){
  • // Pass off to the table click handler.
  • self.onClick( event );
  •  
  • // Cancel default event.
  • return( false );
  • }
  • );
  •  
  • // Initialize the table.
  • this.initTable();
  • };
  •  
  •  
  • // I build the actual markup of the table using the
  • // given number of columns and rows.
  • MineSweeper.prototype.buildTable = function(){
  • // Build the markup for a given row.
  • var rowHtml = ("<tr>" + $.repeatString( "<td class=\"active\">&nbsp;</td>", this.columnCount ) + "</tr>");
  •  
  • // Build the markup for the table using the row
  • // data the given number of times.
  • var tableHtml = $.repeatString( rowHtml, this.rowCount );
  •  
  • // Set the HTML of the table to fill it out.
  • this.table.html( tableHtml );
  • };
  •  
  •  
  • // I check to see if an end-game has been reached.
  • // If so, then I give the option to restart.
  • MineSweeper.prototype.checkEndGame = function(){
  • var message = "";
  • var isEndGame = false;
  •  
  • // Check to see if any of the bombs have exploded.
  • if (this.bombCells.filter( ".bombed" ).size()){
  •  
  • // Set the message.
  • message = "You LOSE - Play Again?";
  •  
  • // Flag the end game.
  • isEndGame = true;
  •  
  • // Check to see if there are any more active
  • // non-bomb cells. If not, then the user has
  • // successfully clicked all non-bomb cells.
  • } else if (!this.nonBombCells.filter( ".active" ).size()){
  •  
  • // Set the message.
  • message = "You WIN - Play Again?";
  •  
  • // Flag the end game.
  • isEndGame = true;
  •  
  • }
  •  
  • // Check to see if the game is over.
  • if (isEndGame){
  •  
  • // Prompt for replay.
  • if (confirm( message )){
  •  
  • // Restart the game.
  • this.restart();
  •  
  • }
  •  
  • }
  • };
  •  
  •  
  • // I clear the table of any markup.
  • MineSweeper.prototype.clearTable = function(){
  • this.table.empty();
  • };
  •  
  •  
  • // I initialize the table.
  • MineSweeper.prototype.initTable = function(){
  • var self = this;
  •  
  • // Clear the table if there is any existing markup.
  • this.clearTable();
  •  
  • // Now that we have ensured that the table is
  • // empty, let's build out the HTML for the table.
  • this.buildTable();
  •  
  • // Gather the cells of the table.
  • this.cells = this.table.find( "td" );
  •  
  • // Set the "near bombs" data for each cell to
  • // zero. This is the number of bombs that the cell
  • // is near.
  • this.cells.data( "nearBombs", 0 );
  •  
  • // For each cell, keep a collection of the cells
  • // that are near this cell.
  • this.cells.each(
  • function( index, cellNode ){
  • var cell = $( this );
  •  
  • // Store the near cells.
  • cell.data( "near", cell.near() );
  • }
  • );
  •  
  • // Randomly select and gather the bomb cells.
  • this.bombCells = this.cells
  • .randomFilter( this.bombCount )
  • .addClass( "bomb" );
  • ;
  •  
  • // Now that we've selected the bomb cells, let's
  • // get teh non-bomb cells.
  • this.nonBombCells = this.cells.filter(
  • function( index ){
  • // If this cell does NOT appear in the bomb
  • // cells collection, then it's a non-bomb
  • // cell.
  • return( self.bombCells.index( this ) == -1 );
  • }
  • );
  •  
  • // Now that we have the bomb cells, let's go through
  • // each of them and apply its "nearness" to the
  • // cells around it.
  • this.bombCells.each(
  • function( index, node ){
  • var cell = $( this );
  • var nearCells = cell.data( "near" );
  •  
  • // For each near cell, increment the near
  • // data counter.
  • nearCells.each(
  • function(){
  • var nearCell = $( this );
  •  
  • // Get the current near data and
  • // increment it.
  • nearCell.data(
  • "nearBombs",
  • (nearCell.data( "nearBombs" ) + 1)
  • );
  • }
  • );
  • }
  • );
  • };
  •  
  •  
  • // I handle the clicks at the table level.
  • MineSweeper.prototype.onClick = function( event ){
  • // Get the trigger for the event.
  • var target = $( event.target );
  •  
  • // Check to make sure the target is an active cell.
  • // If it is not, then we are not interested.
  • if (!target.is( "td.active" )){
  •  
  • // This cell is not of any concern; simply
  • // return out to prevent processing.
  • return;
  •  
  • }
  •  
  •  
  • // Check to see if the ALT key was pressed. If it
  • // was, then we are handling the caution toggle.
  • // If not, then we are going to process a normal
  • // click event.
  • if (event.altKey){
  •  
  • // Toggle the caution nature of this cell.
  • this.toggleCaution( target );
  •  
  • } else {
  •  
  • // Check to see if the target was a bomb cell.
  • if (target.is( ".bomb" )){
  •  
  • // The user clicked on a bomb, which will end
  • // the game. Reveal the whole board (end-game
  • // check comes below).
  • this.revealBoard();
  •  
  • } else {
  •  
  • // The target was not a bomb, so show it.
  • this.revealCell( target );
  •  
  • }
  •  
  • // Check end game.
  • this.checkEndGame();
  •  
  • }
  • };
  •  
  •  
  • // I restart the game.
  • MineSweeper.prototype.restart = function(){
  • // Re-initialize the table.
  • this.initTable();
  • };
  •  
  •  
  • // I reveal the entire board.
  • MineSweeper.prototype.revealBoard = function(){
  • // Remove the transient classes.
  • this.cells
  • .removeClass( "active" )
  • .removeClass( "caution" )
  • ;
  •  
  • // Add the bombed classes to the bombs.
  • this.bombCells.addClass( "bombed" );
  •  
  • // Set the cell contents.
  • this.cells.each(
  • function( index, cellNode ){
  • var cell = $( this );
  •  
  • // Check to see if this is a bomb cell.
  • if (cell.is( ".bomb" )){
  •  
  • // Show an *.
  • cell.html( "*" );
  •  
  • } else if (cell.data( "nearBombs" )){
  •  
  • // Show the count.
  • cell.html( cell.data( "nearBombs" ) );
  •  
  • }
  • }
  • );
  • };
  •  
  •  
  • // I reveal the given cell.
  • MineSweeper.prototype.revealCell = function( cell ){
  • var self = this;
  •  
  • // Remove the active nature of the cell.
  • cell
  • .removeClass( "active" )
  • .removeClass( "caution" )
  • ;
  •  
  • // Check to see if the current cell is near any
  • // bombs. If it is, then we'll just show the
  • // current cell and it's nearness. If not, then
  • // we'll continue to show the surrounding cells.
  • if (cell.data( "nearBombs" )){
  •  
  • // Set the content of the cell.
  • cell.html( cell.data( "nearBombs" ) );
  •  
  • } else {
  •  
  • // Make sure the cell has no markup.
  • cell.html( "&nbsp;" );
  •  
  • // This cell was not near any bombs. Therefore,
  • // it is reasonable to assume the user would
  • // quickly reveal all cells around it. As such,
  • // we will do that for them.
  • cell.data( "near" )
  • .filter( ".active" )
  • .each(
  • function( index, cellNode ){
  • self.revealCell( $( this ) );
  • }
  • )
  • ;
  •  
  • }
  • };
  •  
  •  
  • // I toggle the cautionary nature and display of the
  • // given cell.
  • MineSweeper.prototype.toggleCaution = function( cell ){
  • // Check to see if there is already a caution on it.
  • if (cell.is( ".caution" )){
  •  
  • // Remove caution class.
  • cell.removeClass( "caution" );
  •  
  • // Set appropriate markup.
  • cell.html( "&nbsp;" );
  •  
  • } else {
  •  
  • // Add caution class.
  • cell.addClass( "caution" );
  •  
  • // Set appropriate markup.
  • cell.html( "?" );
  •  
  • }
  • };
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // Store the mine sweeper class in the window scope so
  • // that people can reach it ouside of this bubble.
  • window.MineSweeper = MineSweeper;
  •  
  • })( jQuery );

The rest of the Javascript files are simply jQuery plugins.

This jQuery plugin selects a random value between the two given integers.

jquery.randrange.js

 Launch code in new window » Download code as text file »

  • (function( $ ){
  •  
  • // I get a random value between the min and max values
  • // (inclusive). The min and max values are expected to be
  • // interger values.
  • $.randRange = function( minValue, maxValue ){
  • // Get the range of our random values.
  • var delta = (maxValue - minValue);
  •  
  • // Select a random number for our range and truncate it.
  • var randomValue = Math.floor( Math.random() * delta );
  •  
  • // Get a random value by adding the random selection to
  • // our min value.
  • return( minValue + randomValue );
  • };
  •  
  • })( jQuery );

This jQuery plugin repeats the given string a given number of times. I use this one to create the table markup. Creating the entire markup of the table is faster and more performant than creating individual table cells.

jquery.repeatstring.js

 Launch code in new window » Download code as text file »

  • (function( $ ){
  •  
  • // I repeat the given string the given number of times.
  • $.repeatString = function( value, count ){
  • return(
  • (new Array( count + 1 )).join( value )
  • );
  • };
  •  
  • })( jQuery );

This jQuery plugin randomly filters a collection down to the given size.

jquery.randomfilter.js

 Launch code in new window » Download code as text file »

  • (function( $ ){
  •  
  • // I filter the given collection down to the given size by
  • // randomly selecting the filtered elements.
  • $.fn.randomFilter = function( size ){
  • // Make sure that the size is not more than the size of
  • // the collection.
  • size = Math.min( size, this.size() );
  •  
  • // Build up an array of possible index values.
  • var indexes = new Array( this.size() );
  •  
  • // Loop over the size of the collection and store the
  • // current index value at the corresponding index of
  • // the lookup array.
  • for (var i = 0 ; i < this.size() ; i++){
  • indexes[ i ] = i;
  • }
  •  
  • // Create an object to hold our random indexes.
  • var randomIndexes = {};
  •  
  • // Get the correct number of random index values.
  • for (var i = 0 ; i < size ; i++){
  •  
  • // Select a random index from the lookup array.
  • // Remember that this array will be getting smaller
  • // with each random selection.
  • randomIndex = $.randRange( 0, indexes.length - 1 );
  •  
  • // Add the randomly selected index to the index
  • // collection.
  • randomIndexes[ indexes[ randomIndex ] ] = true;
  •  
  • // Remove the selected index from the available pool.
  • indexes.splice( randomIndex, 1 );
  •  
  • }
  •  
  • // Return the filtered collection.
  • return(
  • this.filter(
  • function( index ){
  • return( index in randomIndexes );
  • }
  • )
  • );
  • };
  •  
  • })( jQuery );

This jQuery plugin selects the table cells around the given cell. This will return, at most, the three cells above, the three cells below, and the cell to the left and right of the given cell.

jquery.near.js

 Launch code in new window » Download code as text file »

  • (function( $ ){
  •  
  • // This is meant to be used on collections of TD elements.
  • // It will get at most the 8 surrounding TD cells.
  • $.fn.near = function(){
  • var nearNodes = $( [] );
  • var currentCell = $( this );
  • var currentRow = currentCell.parent( "tr" );
  • var tbody = currentRow.parent();
  • var prevRow = currentRow.prev();
  • var nextRow = currentRow.next();
  • var currentCellIndex = currentRow.find( "td" ).index( currentCell );
  •  
  • // Check to see if there is a previous row.
  • if (prevRow.size()){
  •  
  • // Grab the cell just above the current cell.
  • var prevRowCell = prevRow.find( "td:eq(" + currentCellIndex + ")" );
  •  
  • // Add the top 3 near cells to the collection that
  • // we are going to return.
  • nearNodes = nearNodes
  • .add( prevRowCell.prev() )
  • .add( prevRowCell )
  • .add( prevRowCell.next() )
  • ;
  •  
  • }
  •  
  • // Add the left / right near cells to the collection
  • // that we are going to return.
  • nearNodes = nearNodes
  • .add( currentCell.prev() )
  • .add( currentCell.next() )
  • ;
  •  
  • // Check to see if there is a next row.
  • if (nextRow.size()){
  •  
  • // Grab the cell just below the current cell.
  • var nextRowCell = nextRow.find( "td:eq(" + currentCellIndex + ")" );
  •  
  • // Add the bottom 3 near cells to the collection that
  • // we are going to return.
  • nearNodes = nearNodes
  • .add( nextRowCell.prev() )
  • .add( nextRowCell )
  • .add( nextRowCell.next() )
  • ;
  •  
  • }
  •  
  • // Return the collection of near cells.
  • return( nearNodes );
  • }
  •  
  • })( jQuery );

I'm not going to go into any further explanation since there's a lot of code here. Mostly, it was just for funzies.

Download Code Snippet ZIP File

Post Comment  |  Ask Ben  |  Other Searches  |  Print Page




Reader Comments

Oct 27, 2009 at 10:28 AM // reply »
21 Comments

Very nice dude. Gave it a try, and it's working great. Fun stuff.


Oct 27, 2009 at 10:33 AM // reply »
14 Comments

Here's the cheat code: http://www.bennadel.com/resources/demo/mine_sweeper/index.cfm?rows=1&cols=1&bombs=0%

Seriously, nice job Ben!


Oct 27, 2009 at 10:34 AM // reply »
7,572 Comments

@Adam,

Thanks my man. I just realized that the "%" in the URL probably needs to be escaped. Hmmm.


Oct 27, 2009 at 10:36 AM // reply »
7,572 Comments

@Pete,

Ha ha ha, thanks :)

Ok, so yeah, to escape percent signs, you need "%25". I have updated the links.


Oct 27, 2009 at 11:32 AM // reply »
3 Comments

Great game, man.

May want to consider using event handlers for onContextMenu / onMouseDown (well, specifically right clicks) instead of using the Alt + Click approach, as it's not working for me in Linux.

When you use alt + click (in Gnome at least), it simply tries to move your window instead of allowing the click to pass through to the browser.

Just letting you know...


Oct 27, 2009 at 11:37 AM // reply »
7,572 Comments

@Dan,

Yeah, it seems like the functional keys do something different on the different browsers. I originally was gonna go with CTRL+Click, but in FireFox, that selects the actual markup element for copy.


Oct 27, 2009 at 11:52 AM // reply »
124 Comments

One thing to keep in mind is since you're using the "bomb" class on the element to mark a "bomb", one can easily use Firebug to find the bombs.

If doing in a real environment (such as for an online contest,) you'd want to avoid using the client-side to store the results. You'd really want to use AJAX to check with the server--to avoid people being able to "cheat."

Just something to keep in mind for anyone who might be using this as inspiration for something mission critical.


Oct 27, 2009 at 11:55 AM // reply »
7,572 Comments

@Dan,

Point taken... but I have to admit that it's funny to see "Mine Sweeper" and "mission critical" in the same blog post ;)


Oct 27, 2009 at 3:43 PM // reply »
5 Comments

Hi Ben
That's awesome. Just had a round and its really neat. Have I missed the jQuery SuDoKu or is it still coming?


Oct 27, 2009 at 3:50 PM // reply »
1 Comments

$("td").each(function() { if(!$(this).hasClass("bomb")) { $(this).click(); }});

:D


Oct 27, 2009 at 3:54 PM // reply »
7,572 Comments

@Jasonmcleod,

Noooooooo! << insert dramatic, slow motion scream >>

@Sreenath,

Hmmm, Sodoku is definitely a complicated game. I am not sure how I would go about creating the actual boards.


Oct 28, 2009 at 7:38 AM // reply »
106 Comments

That's amazing Ben! Great work on this. You do some really interesting stuff with jQuery.


Oct 28, 2009 at 8:55 AM // reply »
28 Comments

That is very cool. Only one feature is missing. (If you care to add it).

I heard that the original MineSweeper has logic built in to always leave the bottom right corner free, and if the first click is on a mine, it moves the mine to that corner. Thus preventing any one click losses. Though, I'm sure that may be a bit outside the scope of this example, but still could be a fun exercise.


Oct 28, 2009 at 12:05 PM // reply »
1 Comments

Wow, I hadn't thought about it that way before. Good write up, very clearly written. Have you written previously about jQuery Powered Mine Sweeper Game? I'd love to read more.


Oct 28, 2009 at 1:29 PM // reply »
7,572 Comments

@Tim,

Ah, very cool idea. I could probably figure something along those lines out.


Oct 28, 2009 at 1:36 PM // reply »
106 Comments

I don't think the "moving the mine" is accurate. There have been many times where I've clicked once and got blown up (especially once you start getting a lot of mines on the screen).


Oct 28, 2009 at 2:00 PM // reply »
28 Comments

I was wrong about the corner, but this says it's true:
http://www.techuser.net/mineclick.html


Oct 28, 2009 at 2:03 PM // reply »
7,572 Comments

@Tim,

No worries. Still an interesting behavior.


Oct 28, 2009 at 2:04 PM // reply »
28 Comments

..or are we completely missing the forest by getting stuck on the actual first click logic of minesweeper, and forgetting the awesomeness of the fact that Ben did a really snazzy job at emulating this game.


Oct 28, 2009 at 4:54 PM // reply »
23 Comments

Very cool game :)


Oct 31, 2009 at 2:19 PM // reply »
7,572 Comments

@Tim, @JC,

Thanks guys :)


Dec 22, 2009 at 4:56 AM // reply »
1 Comments

Tweeted your post!

I have downloaded the code and hope to explore it very soon. Especially I would like to see the code flow in jquery- I would learn a lot from your code!

Thank you.


Post Comment  |  Ask Ben

Recent Blog Comments
Mar 21, 2010 at 3:59 PM
Exploring ColdFusion Component Runtime Class Properties And Serialization
@Elliott, according to Ben's experiment, serializeJSON() doesn't access the private data by default - it doesn't even access the getHair() method - so trying to clone a Girl.cfc via serializeJSON/des ... read »
Mar 21, 2010 at 3:49 PM
Ask Ben: Javascript String Replace Method
I'm confused a bit by what you are asking, but if had this sentence: The color, red, is in the style statement; style: red;. and wanted to remove all or change all of the commas, colons, and semi-c ... read »
Mar 21, 2010 at 3:13 PM
Ask Ben: Javascript String Replace Method
I am trying to make a java program to count the number of times that these punctuation marks occur in a body of text: , : ; . ! - ' " ? / \ I am using this piece to ferret out the commas: numcommas ... read »
Mar 21, 2010 at 11:13 AM
A New Wrist Pain
@chiropractor suwanee, Spoken like someone trying to sell something. Other than for minor, temporary relief from some back pain, chiropractic treatment is nothing but placebo effect and quackery. ... read »
Mar 21, 2010 at 6:32 AM
ColdFusion CFPOP - My First Look
Apologies... The field name in the db for C. is "BounceCode" It stores the code / message which is returned in the email. Sorry for the confusion. ... read »
Mar 21, 2010 at 6:29 AM
ColdFusion CFPOP - My First Look
@Jose Galdamez, Hi Ben and Jose 1st of all.. big thanks to Jose for his Skype chat a few weeks back. Your time was much appreciated. I have come up with a rather unelegant solution to my problem a ... read »
Mar 21, 2010 at 3:42 AM
A New Wrist Pain
Chiropractic treatment is one of the best methods for treating numerous health problems naturally. After years of experience being a chiropractor, I have found that it is a powerful way to solve many ... read »
Mar 20, 2010 at 12:07 PM
Drawing On The iPhone Canvas With jQuery And ColdFusion
Simply awesome. Saved my day. ... read »