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  |  Permalink  |  Other Searches  |  Print Page



Learning ColdFusion 9 - ColdFusion 9 tutorials, samples, examples, demos

Reader Comments

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

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


Oct 27, 2009 at 10:33 AM // reply »
13 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 »
6,516 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 »
6,516 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 »
2 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 »
6,516 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 »
111 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 »
6,516 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 »
2 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 »
6,516 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 »
102 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 »
27 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 »
6,516 Comments

@Tim,

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


Oct 28, 2009 at 1:36 PM // reply »
102 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 »
27 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 »
6,516 Comments

@Tim,

No worries. Still an interesting behavior.


Oct 28, 2009 at 2:04 PM // reply »
27 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 »
6,516 Comments

@Tim, @JC,

Thanks guys :)


Post Comment  |  Ask Ben

Recent Blog Comments
Nov 22, 2009 at 8:55 AM
Project HUGE: Trying Out A 4-Exercise Limit
Very informative. Thanks for the great post. ... read »
aha
Nov 22, 2009 at 7:42 AM
Using A Name Suffix In ColdFusion's CFMail Tag
Why not? ... read »
Nov 22, 2009 at 7:37 AM
Using A Name Suffix In ColdFusion's CFMail Tag
asd ... read »
Nov 22, 2009 at 4:30 AM
jQuery Live() Method And Event Bubbling
dasegtezr ... read »
Nov 22, 2009 at 4:03 AM
jQuery Live() Method And Event Bubbling
C_fieri ... read »
Nov 22, 2009 at 1:56 AM
Learning ColdFusion 9: Using CFQuery In CFScript Can Enable SQL Injection Attacks
Why adobe would give you script equivalent of cfquery is beyond me. I love cfquery tag because it helps me wriite clean sql, and get away from the horrible jdbc queries If I wanted to write javali ... read »
Nov 22, 2009 at 1:45 AM
Streaming Text Using ColdFusion's CFContent Tag And The Variable Attribute
The reason you would want to do this is to stream. Ack json/xml files to ria clients I used thus technique before because putting json in response stream causes debugging info to come thru As well a ... read »
Nov 21, 2009 at 6:47 PM
Hal Helms - Real World Object Oriented Development, Sarasota - Day Five
@charlie griefer, Thank you.. ... read »